"""在 sw8/sw9 专用测试账号下核对写流程;临时渠道、绑定、预警及规则在 finally 中清理。""" import argparse import json import time import urllib.error import urllib.parse import urllib.request from pathlib import Path import pymysql import yaml from sw_api_compare import compare_data class Rehearsal: def __init__(self, args): self.args = args self.checks = [] args.output.write_text(json.dumps({"checks": [], "passed": False, "status": "running"}) + "\n", encoding="utf-8") self.root = args.runtime self.users = json.loads((self.root / "synthetic-test-users.json").read_text(encoding="utf-8")) self.tokens = {(side, identity): (self.root / f"{side}-{identity}.token").read_text().strip() for side in ("legacy", "new") for identity in ("admin", "level3", "testlevel1", "testlevel2", "testlevel3")} self.client = json.loads((self.root / "legacy-client.json").read_text())["client_id"] maps = json.loads((self.root / "id-maps.json").read_text()) self.mapping = {str(row["old_id"]): str(row["new_id"]) for row in maps if row["entity"] == "user"} config = yaml.safe_load(args.config.read_text(encoding="utf-8"))["data"]["database"] self.db = pymysql.connect(host=config["host"], port=int(config["port"]), user=config["user"], password=config["password"], autocommit=True, charset="utf8mb4", read_timeout=60, cursorclass=pymysql.cursors.DictCursor) self.rule_ids = {} self.suffix = str(int(time.time())) self.rule_type = "mig_verify_" + self.suffix self.account = "mig_verify_" + self.suffix self.promote_id = 2000000000 + int(self.suffix[-7:]) self.alert_id = 9007199254740993 + int(self.suffix[-7:]) self.fixtures = [] def query(self, sql, params=()): with self.db.cursor() as cursor: cursor.execute(sql, params) return cursor.fetchall() def check(self, name, passed, **details): self.checks.append({"check": name, "passed": bool(passed), **details}) self.args.output.write_text(json.dumps({"checks": self.checks, "passed": all(item["passed"] for item in self.checks)}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(self.checks[-1], ensure_ascii=False), flush=True) if not passed: raise AssertionError(name) def call(self, side, identity, method, path, payload=None, success=True): old = side == "legacy" headers = {"Content-Type": "application/json"} headers.update({"Authorization": "Bearer " + self.tokens[side, identity], "clientid": self.client} if old else {"x-token": self.tokens[side, identity]}) request = urllib.request.Request((self.args.old_url if old else self.args.new_url) + path, data=json.dumps(payload).encode() if payload is not None else None, method=method, headers=headers) try: with urllib.request.urlopen(request, timeout=60) as response: raw = response.read() status = response.status except urllib.error.HTTPError as response: raw = response.read() status = response.code try: reply = json.loads(raw) except (ValueError, UnicodeDecodeError): self.check(path + " " + side, False, http=status, response_bytes=len(raw), error="响应不是 JSON") if success and reply.get("code") != (200 if old else 0): self.check(path + " " + side, False, code=reply.get("code"), message=reply.get("msg")) return reply def owner(self, side, level): return self.users[level - 1]["source_id" if side == "legacy" else "target_id"] def prefix(self, side): return "/system" if side == "legacy" else "/sw" def rule_snapshot(self, name): values = [self.call(side, "testlevel1", "GET", self.prefix(side) + "/alertRule/" + str(self.rule_ids[side]))["data"] for side in ("legacy", "new")] options = {"source_omit_fields": ["id", "createdAt", "updatedAt"], "target_omit_fields": ["id", "createdAt", "updatedAt"], "null_empty_fields": ["momDirectionLabel"]} result = compare_data(*values, options, self.mapping) self.check(name, result["semantic_equal"], differences=result["differences"]) return values def rules(self): self.rule = {"ruleName": "迁移核对临时规则", "ruleType": self.rule_type, "monitorMetric": 3, "unloginDays": 2, "monitorScope": 1, "warningColor": "#f56c6c"} for side, database in (("legacy", "sw8"), ("new", "sw9")): self.call(side, "testlevel1", "POST", self.prefix(side) + "/alertRule/add", self.rule) rows = self.query(f"SELECT id FROM {database}.sys_alert_rule WHERE creator_id=%s AND rule_type=%s", (self.owner(side, 1), self.rule_type)) self.check("新增规则 " + side, len(rows) == 1) self.rule_ids[side] = rows[0]["id"] self.rule_snapshot("新增规则字段一致") for side in ("legacy", "new"): self.call(side, "testlevel1", "PUT", self.prefix(side) + f"/alertRule/toggle/{self.rule_ids[side]}?enabled=0") values = self.rule_snapshot("规则禁用一致") self.check("禁用状态生效", all(row["enabled"] == 0 for row in values)) self.rule.update(monitorMetric=1, statPeriodDays=3, momDirection=2, momThreshold=12.5) self.rule.pop("unloginDays") for side in ("legacy", "new"): self.call(side, "testlevel1", "PUT", self.prefix(side) + "/alertRule/edit", {**self.rule, "id": self.rule_ids[side]}) values = self.rule_snapshot("编辑保留启停和可选历史字段") self.check("编辑未重启规则且保留未登录天数", all(row["enabled"] == 0 and row["unloginDays"] == 2 for row in values)) for side in ("legacy", "new"): rejected = self.call(side, "testlevel2", "PUT", self.prefix(side) + f"/alertRule/toggle/{self.rule_ids[side]}?enabled=1", success=False) self.check("二级不能修改一级规则 " + side, rejected.get("code") != (200 if side == "legacy" else 0)) self.call(side, "testlevel1", "PUT", self.prefix(side) + f"/alertRule/toggle/{self.rule_ids[side]}?enabled=1") self.rule_snapshot("重新启用规则一致") def prepare_alert(self): for side, database in (("legacy", "sw8"), ("new", "sw9")): existing = self.query(f"SELECT id FROM {database}.tab_promote WHERE id=%s AND source_db='lx'", (self.promote_id,)) self.check("临时渠道 ID 未占用 " + side, not existing) existing = self.query(f"SELECT id FROM {database}.sys_alert_record WHERE id=%s", (self.alert_id,)) self.check("临时预警 ID 未占用 " + side, not existing) self.fixtures.append({"side": side, "database": database, "promote_id": self.promote_id, "alert_id": self.alert_id, "account": self.account, "owner": self.owner(side, 1), "user": self.owner(side, 3)}) (self.root / "write-fixtures.json").write_text(json.dumps(self.fixtures, indent=2), encoding="utf-8") self.query(f"INSERT INTO {database}.tab_promote(id,source_db,account,busier_id,promote_level,status) VALUES(%s,'lx',%s,%s,1,1)", (self.promote_id, self.account, self.promote_id)) self.query(f"INSERT INTO {database}.sys_user_bind_platform(user_id,source_db,platform_busier_id,platform_busier_account,status,create_time) VALUES(%s,'lx',%s,%s,'0',NOW())", (self.owner(side, 3), self.promote_id, self.account)) self.query(f"INSERT INTO {database}.sys_alert_record(id,platform_code,platform_name,rule_id,rule_name,rule_type,monitor_metric,target_promote_id,target_promote_name,business_id,business_name,alert_content,status,alert_date,created_at,updated_at) VALUES(%s,'lx','龙翔',%s,%s,%s,1,%s,%s,%s,%s,'迁移核对临时预警',1,CURDATE(),NOW(),NOW())", (self.alert_id, self.rule_ids[side], self.rule["ruleName"], self.rule_type, self.promote_id, self.account, self.owner(side, 1), self.users[0]["username"])) def alert_snapshot(self, label, expected_status): values = [self.call(side, "testlevel1", "GET", self.prefix(side) + f"/alertRecord/detail/{self.alert_id}")["data"] for side in ("legacy", "new")] for row in values: row["alertRecord"].pop("ruleId", None) options = {"source_omit_fields": ["id", "createdAt", "updatedAt"], "target_omit_fields": ["id", "createdAt", "updatedAt"], "null_empty_fields": ["actionDetail", "noteContent"]} # 日志主键独立生成;按原操作序列比较业务字段及操作人映射。 result = compare_data(*values, options, self.mapping) self.check(label, result["semantic_equal"] and all(row["alertRecord"]["status"] == expected_status for row in values), status=expected_status, log_counts=[len(row["operationLogs"]) for row in values], differences=result["differences"]) def workflow(self): self.prepare_alert() self.alert_snapshot("临时预警初始状态一致", 1) for side in ("legacy", "new"): rejected = self.call(side, "level3", "GET", self.prefix(side) + f"/alertRecord/detail/{self.alert_id}", success=False) self.check("其他商务不可查看临时预警 " + side, rejected.get("code") != (200 if side == "legacy" else 0)) steps = [("startProcess", "testlevel3", 2), ("submitAudit", "testlevel3", 3), ("reject", "testlevel1", 4), ("reProcess", "testlevel3", 2), ("submitAudit", "testlevel3", 3), ("approve", "testlevel1", 5)] for index, (action, identity, status) in enumerate(steps): for side in ("legacy", "new"): self.call(side, identity, "POST", self.prefix(side) + "/alertRecord/" + action, {"alertId": str(self.alert_id), "note": "迁移核对步骤" + str(index)}) self.alert_snapshot(action + " 状态及日志一致", status) if index == 1: for side in ("legacy", "new"): rejected = self.call(side, "testlevel3", "POST", self.prefix(side) + "/alertRecord/approve", {"alertId": str(self.alert_id)}, success=False) self.check("三级商务不能审核 " + side, rejected.get("code") != (200 if side == "legacy" else 0)) for side in ("legacy", "new"): self.call(side, "testlevel3", "POST", self.prefix(side) + "/alertRecord/addNote", {"alertId": str(self.alert_id), "noteContent": "迁移核对独立备注", "attachmentIds": []}) self.alert_snapshot("已处理记录追加备注不改变状态", 5) def remove_rule(self): for side, database in (("legacy", "sw8"), ("new", "sw9")): self.call(side, "testlevel1", "DELETE", self.prefix(side) + "/alertRule/" + str(self.rule_ids[side])) row = self.query(f"SELECT is_deleted FROM {database}.sys_alert_rule WHERE id=%s", (self.rule_ids[side],))[0] self.check("删除规则保留历史行 " + side, row["is_deleted"] == 1) def cleanup(self): for fixture in reversed(self.fixtures): database = fixture["database"] self.query(f"DELETE FROM {database}.sys_alert_operation_log WHERE alert_id=%s", (fixture["alert_id"],)) self.query(f"DELETE FROM {database}.sys_alert_record WHERE id=%s AND target_promote_name=%s", (fixture["alert_id"], fixture["account"])) self.query(f"DELETE FROM {database}.sys_user_bind_platform WHERE user_id=%s AND source_db='lx' AND platform_busier_id=%s AND platform_busier_account=%s", (fixture["user"], fixture["promote_id"], fixture["account"])) self.query(f"DELETE FROM {database}.tab_promote WHERE id=%s AND source_db='lx' AND account=%s", (fixture["promote_id"], fixture["account"])) for side, database in (("legacy", "sw8"), ("new", "sw9")): self.query(f"DELETE FROM {database}.sys_alert_rule WHERE creator_id=%s AND rule_type=%s", (self.owner(side, 1), self.rule_type)) remaining = 0 for fixture in self.fixtures: database = fixture["database"] for table, condition, params in ( ("sys_alert_record", "id=%s", (fixture["alert_id"],)), ("sys_alert_operation_log", "alert_id=%s", (fixture["alert_id"],)), ("tab_promote", "id=%s AND source_db='lx'", (fixture["promote_id"],)), ("sys_user_bind_platform", "user_id=%s AND source_db='lx' AND platform_busier_id=%s", (fixture["user"], fixture["promote_id"])), ): remaining += self.query(f"SELECT COUNT(*) n FROM {database}.{table} WHERE {condition}", params)[0]["n"] for side, database in (("legacy", "sw8"), ("new", "sw9")): remaining += self.query(f"SELECT COUNT(*) n FROM {database}.sys_alert_rule WHERE creator_id=%s AND rule_type=%s", (self.owner(side, 1), self.rule_type))[0]["n"] self.check("临时预警渠道绑定和规则已清理", remaining == 0, remaining=remaining) self.db.close() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--runtime", type=Path, required=True) parser.add_argument("--config", type=Path, default=Path("configs/config.yaml")) parser.add_argument("--old-url", default="http://127.0.0.1:18080") parser.add_argument("--new-url", default="http://127.0.0.1:19001") parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() if args.runtime.resolve().is_relative_to(Path(__file__).resolve().parents[1]): raise ValueError("测试状态必须保存在仓库外") run = Rehearsal(args) try: run.rules() run.workflow() run.remove_rule() finally: run.cleanup() if __name__ == "__main__": main()