208 lines
14 KiB
Python
208 lines
14 KiB
Python
"""在 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()
|