"""已签发授权台账测试(厂商侧 Registry)。 重点验证: 1. 由 license 文档派生记录(唯一写入口,不手工拼记录) 2. 状态判定(永久/有效/即将到期/已过期/未生效)与剩余天数 3. 检索:客户 / 机器码 / 插件 / 关键字 / 状态 4. **续期复用台账里的来源指纹** —— 台账最大的价值:不必再向客户索取申请码 5. 重新导入不覆盖人工维护的备注 6. 导出 .lic(按客户分子目录)与 CSV(UTF-8 BOM,Excel 中文不乱码) 7. 结构版本不匹配时明确报错,而不是让查询诡异失败 运行:`python test/test_license_registry.py`,或 `uv run pytest test/test_license_registry.py`。 """ import csv import sqlite3 import sys import tempfile from datetime import date, timedelta from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # noqa: E402 from chongming_license import ( # noqa: E402 Registry, RegistryError, build_license, build_payload, machine_code, public_key_b64, public_key_fingerprint, record_from_license, ) failures: list[str] = [] def check(name, cond, detail=""): print(f"[{'PASS' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else "")) if not cond: failures.append(name) PRIV = Ed25519PrivateKey.generate() PUB = public_key_b64(PRIV) FP = public_key_fingerprint(PUB) SRC = {"board_serial": "sn123456789", "emmc_cid": "1a2b3c4d", "mac": "aabbccddeeff"} CODE = machine_code(SRC) def make_doc(*, customer="某某科技", days=300, plugins=("aibox_sophon_adapter",), note="", machine=None, sources=None, lic_id="", not_before=None): payload = build_payload( machine_code=machine or CODE, plugins=list(plugins), sources=SRC if sources is None else sources, customer=customer, note=note, lic_id=lic_id, not_before=not_before, expires_at=(date.today() + timedelta(days=days)) if days is not None else None, ) return build_license(payload, PRIV, kid="vendor-2026") def test_record_derivation(): """记录完全由 license 文档派生,查询列与原始文档不会不一致。""" doc = make_doc(customer="派生测试", note="合同 X", plugins=["a", "b"]) rec = record_from_license(doc, public_key_fp=FP, source="test") payload = doc["payload"] check("lic_id 取自载荷", rec.lic_id == payload["lic_id"], rec.lic_id) check("机器码大写归一", rec.machine_code == payload["machine_code"].upper()) check("插件列表解析", rec.plugins == ["a", "b"], str(rec.plugins)) check("来源指纹解析", rec.sources == SRC, str(sorted(rec.sources))) check("kid 取自信封", rec.kid == "vendor-2026") check("公钥指纹记录在案", rec.public_key_fp == FP) check("可还原完整文档", rec.license_doc()["sig"] == doc["sig"]) # 日期只保留日期部分:否则 SQL 里的字符串比较会得出错误排序 doc2 = make_doc() doc2["payload"]["expires_at"] = "2027-09-01T12:34:56Z" check("日期裁剪到日粒度", record_from_license(doc2).expires_at == "2027-09-01", str(record_from_license(doc2).expires_at)) # 缺 lic_id 必须报错,而不是静默写一条垃圾记录 try: record_from_license({"payload": {"machine_code": "X"}}) check("缺 lic_id 时报错", False, "未抛异常") except RegistryError: check("缺 lic_id 时报错", True) def test_status_and_days_left(): """五种状态的判定(含边界:到期当天仍有效)。""" tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: reg.save(make_doc(lic_id="L-FOREVER", days=None), public_key_fp=FP) reg.save(make_doc(lic_id="L-ACTIVE", days=300), public_key_fp=FP) reg.save(make_doc(lic_id="L-EXPIRING", days=10), public_key_fp=FP) reg.save(make_doc(lic_id="L-EXPIRED", days=-1), public_key_fp=FP) reg.save(make_doc(lic_id="L-TODAY", days=0), public_key_fp=FP) reg.save(make_doc(lic_id="L-FUTURE", days=100, not_before=(date.today() + timedelta(days=30)).isoformat()), public_key_fp=FP) got = {r.lic_id: r.status() for r in reg.query()} check("无到期日 → forever", got["L-FOREVER"] == "forever", got["L-FOREVER"]) check("远期 → active", got["L-ACTIVE"] == "active", got["L-ACTIVE"]) check("30 天内 → expiring", got["L-EXPIRING"] == "expiring", got["L-EXPIRING"]) check("已过 → expired", got["L-EXPIRED"] == "expired", got["L-EXPIRED"]) check("到期当天仍有效(含当天)", got["L-TODAY"] == "expiring", got["L-TODAY"]) check("未到生效日 → not_yet_valid", got["L-FUTURE"] == "not_yet_valid", got["L-FUTURE"]) check("剩余天数为负表示已过期", reg.get("L-EXPIRED").days_left() < 0, str(reg.get("L-EXPIRED").days_left())) check("永久授权剩余天数为 None", reg.get("L-FOREVER").days_left() is None) st = reg.stats() check("概览计数一致", st == {"total": 6, "expired": 1, "expiring": 2, "active": 1, "forever": 1, "not_yet_valid": 1}, str(st)) def test_query_filters(): """检索:客户 / 机器码 / 插件 / 关键字 / 状态,以及排序。""" tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: reg.save(make_doc(lic_id="L-A", customer="阿尔法科技", days=300, plugins=["doubao"], note="HT-001"), public_key_fp=FP) reg.save(make_doc(lic_id="L-B", customer="贝塔公司", days=10, plugins=["ip_sound_column"], note="HT-002"), public_key_fp=FP) reg.save(make_doc(lic_id="L-C", customer="贝塔公司", days=200, plugins=["doubao_extended"], note="HT-003"), public_key_fp=FP) other = "AAAA-BBBB-CCCC-DDDD-EEEE-FFFF" reg.save(make_doc(lic_id="L-D", customer="伽马", days=300, machine=other, sources={"mac": "aabbccddeeff"}), public_key_fp=FP) check("按客户模糊匹配", {r.lic_id for r in reg.query(customer="贝塔")} == {"L-B", "L-C"}) # L-C 装的是 doubao_extended,查 doubao 时不应被命中(前缀同名不算同一插件) check("按插件匹配不误伤前缀同名", {r.lic_id for r in reg.query(plugin="doubao")} == {"L-A"}, str([r.lic_id for r in reg.query(plugin="doubao")])) check("按插件匹配到前缀同名者本身", {r.lic_id for r in reg.query(plugin="doubao_extended")} == {"L-C"}) check("按机器码匹配", {r.lic_id for r in reg.query(machine_code=CODE[:16])} == {"L-A", "L-B", "L-C"}) check("按备注关键字匹配", {r.lic_id for r in reg.query(keyword="HT-002")} == {"L-B"}) check("大小写不敏感(机器码)", {r.lic_id for r in reg.query(machine_code=other.lower())} == {"L-D"}) order = [r.lic_id for r in reg.query()] check("按到期日升序(急的在前)", order[0] == "L-B", str(order)) # 某台机器的全部历史授权 check("按机器码取全部历史", len(reg.find_by_machine_code(CODE)) == 3, str(len(reg.find_by_machine_code(CODE)))) check("limit 生效", len(reg.query(limit=2)) == 2) def test_renew_reuses_sources_without_request_code(): """核心能力:续期复用台账里的指纹,无需再向客户索取申请码。""" tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: old = reg.save(make_doc(lic_id="L-OLD", customer="续期客户", days=5, plugins=["aibox_sophon_adapter", "ip_sound_column"], note="合同 HT-100"), public_key_fp=FP) check("原授权即将到期", old.status() == "expiring", old.status()) # 模拟现场:客户已联系不上,只有台账记录可用 renewed_doc = make_doc(lic_id="L-NEW", customer=old.customer, days=365, plugins=old.plugins, note=old.note, machine=old.machine_code, sources=old.sources) new = reg.save(renewed_doc, public_key_fp=FP, source="renew") check("续期成功", new.status() == "active", new.status()) check("指纹与原件一致(未索取新申请码)", new.sources == old.sources) check("机器码不变", new.machine_code == old.machine_code) check("新旧授权都在台账(可追溯)", {r.lic_id for r in reg.find_by_machine_code(old.machine_code)} == {"L-OLD", "L-NEW"}) def test_note_preserved_on_reimport(): """重新导入同一份 license 不应覆盖人工维护的备注。""" tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: doc = make_doc(lic_id="L-N", note="原始备注") reg.save(doc, public_key_fp=FP) check("备注已入库", reg.get("L-N").note == "原始备注") reg.update_note("L-N", "人工补充:已电话确认续期") check("备注可更新", reg.get("L-N").note == "人工补充:已电话确认续期") created = reg.get("L-N").created_at reg.save(doc, public_key_fp=FP) # 重新导入(载荷里还是旧备注) after = reg.get("L-N") check("重新导入不覆盖人工备注", after.note == "人工补充:已电话确认续期", after.note) check("创建时间保持不变", after.created_at == created, after.created_at) check("更新时间被推进", after.updated_at >= created) def test_export_and_csv(): """导出 .lic(可按客户分目录)与 CSV(UTF-8 BOM,Excel 不乱码)。""" tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: reg.save(make_doc(lic_id="L-X1", customer="甲方/科技", days=100), public_key_fp=FP) reg.save(make_doc(lic_id="L-X2", customer="乙方", days=-3), public_key_fp=FP) files = reg.export_licenses(tmp / "arc", subdir_by_customer=True) check("导出数量正确", len(files) == 2, str([f.name for f in files])) check("客户名含非法字符时安全替换", any("甲方" in str(f) for f in files), str([str(f.parent.name) for f in files])) # 导出的文件应能被校验端直接读取(验证导出内容不是"看着像"而已) # 注意顺序按到期日升序:files[0] 是已过期的 L-X2,files[1] 是有效的 L-X1 from chongming_license import read_license valid_file = next(f for f in files if "L-X1" in f.name) expired_file = next(f for f in files if "L-X2" in f.name) st = read_license(valid_file, PUB, sources=SRC) check("导出的有效授权仍可验签通过", st.ok, st.message) st2 = read_license(expired_file, PUB, sources=SRC) check("导出的已过期授权被判定为过期(到期日完整保留)", not st2.ok and st2.code == "expired", f"{st2.code} / {st2.message}") check("导出内容可原样还原", valid_file.read_text(encoding="utf-8").lstrip().startswith("{")) csv_path = reg.export_csv(tmp / "台账.csv") with csv_path.open(encoding="utf-8-sig", newline="") as f: rows = list(csv.reader(f)) check("CSV 含表头 + 两条数据", len(rows) == 3, str(len(rows))) check("CSV 表头含状态与到期日", "状态" in rows[0] and "到期日期" in rows[0], str(rows[0])) check("CSV 记录了状态文案", any("已过期" in r for r in rows[1:]), str(rows[1:])) check("CSV 以 UTF-8 BOM 写入(Excel 中文不乱码)", csv_path.read_bytes()[:3] == b"\xef\xbb\xbf") def test_schema_version_mismatch_is_explicit(): """结构版本不匹配时明确报错,而不是让后续查询诡异失败。""" tmp = Path(tempfile.mkdtemp()) path = tmp / "r.db" with Registry(path) as reg: reg.save(make_doc(lic_id="L-V"), public_key_fp=FP) # 手工把版本改成未来值,模拟"被更新版本的工具写过" conn = sqlite3.connect(str(path)) conn.execute("PRAGMA user_version = 99") conn.commit() conn.close() try: Registry(path) check("版本不匹配时拒绝打开", False, "未抛异常") except RegistryError as exc: check("版本不匹配时拒绝打开", "99" in str(exc), str(exc).splitlines()[0]) # 已存在但 user_version=0 的库(旧文件)应被平滑接管 path2 = tmp / "old.db" conn = sqlite3.connect(str(path2)) conn.executescript( "CREATE TABLE issued_licenses (lic_id TEXT PRIMARY KEY, machine_code TEXT, " "customer TEXT, note TEXT, plugins_json TEXT, sources_json TEXT, not_before TEXT, " "expires_at TEXT, issued_at TEXT, kid TEXT, public_key_fp TEXT, license_json TEXT, " "source TEXT, created_at TEXT, updated_at TEXT);" ) conn.commit() conn.close() with Registry(path2) as reg2: check("user_version=0 的旧库被平滑接管", reg2.stats()["total"] == 0) def test_concurrent_open(): """两个实例同时打开同一库(多个 GUI 窗口 / CLI 并行)不应互相报错。""" tmp = Path(tempfile.mkdtemp()) path = tmp / "r.db" a = Registry(path) b = Registry(path) try: a.save(make_doc(lic_id="L-C1", customer="并发A"), public_key_fp=FP) b.save(make_doc(lic_id="L-C2", customer="并发B"), public_key_fp=FP) check("两实例各自写入后互相可见", {r.lic_id for r in a.query()} == {"L-C1", "L-C2"}, str([r.lic_id for r in a.query()])) check("B 能看到 A 的记录", b.get("L-C1") is not None) finally: a.close() b.close() def test_delete(): tmp = Path(tempfile.mkdtemp()) with Registry(tmp / "r.db") as reg: reg.save(make_doc(lic_id="L-DEL"), public_key_fp=FP) check("删除成功", reg.delete("L-DEL") is True) check("删除后查不到", reg.get("L-DEL") is None) check("删除不存在的返回 False", reg.delete("L-DEL") is False) def main(): tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in tests: print(f"\n--- {fn.__name__} ---") fn() print("\n" + "=" * 56) if failures: print(f"❌ {len(failures)} 项未通过: {failures}") return 1 print("✅ 全部通过") return 0 if __name__ == "__main__": sys.exit(main())