Files
chongming-license-gui/smoke_test.py
T
2026-09-21 14:06:55 +08:00

285 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""GUI 冒烟测试(无头运行,驱动真实控件而非只构造窗口)。
覆盖:生成密钥对(含 0600 权限)→ 私钥状态跨页同步 → 解析申请码(含 weak 标注)
→ 插件列表自动发现 → 载荷预览 → 签发 → 查看/校验(目标机通过、异机拒绝)
→ 有效期三种模式的取值。
运行(需要 PySide6,故用本包的独立环境)::
cd utils/license-gui
uv run python smoke_test.py
注意:界面里的信息框/确认框会**阻塞事件循环等待点击**,无头环境下必须先替换掉,
否则测试会挂住。这也是本测试唯一需要 monkeypatch 的地方。
"""
from __future__ import annotations
import json
import sys
import tempfile
from pathlib import Path
from PySide6.QtCore import QSettings
from PySide6.QtWidgets import QApplication
failures: list[str] = []
def check(name: str, cond: bool, detail: str = "") -> None:
print(f"[{'PASS' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
def main() -> int:
# QApplication 实例必须保活,否则控件无法创建;引用由 Qt 内部持有,故不进变量
QApplication.instance() or QApplication([])
QSettings("chongming", "license-gui").clear() # 初始状态确定,不受上次运行影响
from chongming_license import (
build_license,
collect_fingerprints,
encode_request_code,
machine_code,
read_license,
write_license,
)
from chongming_license_gui import (
common,
inspect_tab,
issue_tab,
keys_tab,
registry_tab,
)
from chongming_license_gui.app import MainWindow
# 把模态弹窗换成记录器(否则会阻塞等待点击)
dialogs: list[tuple[str, str]] = []
def _mk(kind: str):
def _f(parent, title, text, *a, **k):
dialogs.append((kind, title))
return True
return _f
for mod in (keys_tab, issue_tab, inspect_tab, registry_tab):
mod.info = _mk("info")
mod.warn = _mk("warn")
mod.error = _mk("error")
if hasattr(mod, "ask_overwrite"):
mod.ask_overwrite = _mk("ask")
common.ask_overwrite = _mk("ask")
w = MainWindow()
w.show()
check("四个页面就绪",
[w.tabs.tabText(i) for i in range(w.tabs.count())]
== ["签发", "台账", "密钥", "查看 / 校验"],
str([w.tabs.tabText(i) for i in range(w.tabs.count())]))
# ---- 1) 生成密钥对 ----
tmp = Path(tempfile.mkdtemp())
priv = tmp / "priv.pem"
w.keys_tab.out_edit.setText(str(priv))
w.keys_tab._generate()
check("私钥已生成并加载", w.signing_key.loaded and priv.is_file())
check("私钥权限为 0600", (priv.stat().st_mode & 0o777) == 0o600,
oct(priv.stat().st_mode & 0o777))
check("公钥指纹已算出", len(w.signing_key.fingerprint) == 16,
w.signing_key.fingerprint)
check("标题显示公钥指纹", w.signing_key.fingerprint in w.windowTitle(),
w.windowTitle())
# ---- 2) 私钥状态跨页同步 ----
check("签发页同步到私钥", w.signing_key.fingerprint in w.issue_tab.key_fp.text(),
w.issue_tab.key_fp.text())
# ---- 3) 解析申请码 ----
src = collect_fingerprints()
rc = encode_request_code(src)
it = w.issue_tab
it.rc_edit.setPlainText(rc)
it._parse_request_code()
check("解析出机器码", it._machine_code == machine_code(src), it._machine_code)
check("解析出来源指纹", it._sources == src, str(sorted(it._sources)))
check("weak 来源被标注为不参与判定",
"不参与判定" in it.target_result.text(), it.target_result.text())
check("来源明细已展示",
len(it.sources_view.toPlainText().splitlines()) == len(src))
# ---- 4) 插件列表自动发现 ----
types = [it.plugin_list.item(i).text() for i in range(it.plugin_list.count())]
check("从 config/workers 自动列出插件", types == ["aibox_sophon_adapter", "ip_sound_column"],
str(types))
it._set_all_checked(True)
check("全选生效", it._checked_plugins() == types, str(it._checked_plugins()))
# 未勾选插件时应拒绝生成
it._set_all_checked(False)
it._refresh_preview()
check("未勾选插件时预览给出提示",
"至少勾选一个" in it.preview.toPlainText(), it.preview.toPlainText()[:40])
it._set_all_checked(True)
# ---- 5) 载荷预览 ----
it.customer_edit.setText("GUI 测试客户")
it.note_edit.setText("合同备注")
it.days_spin.setValue(30)
it._refresh_preview()
preview = json.loads(it.preview.toPlainText())
check("预览含全部授权插件", preview["plugins"] == types, str(preview["plugins"]))
check("预览含客户名", preview["customer"] == "GUI 测试客户")
check("预览含来源指纹", preview["sources"] == src)
# ---- 6) 签发 ----
out = tmp / "license.lic"
payload = it._build_payload()
doc = build_license(payload, w.signing_key.key, kid="gui-test")
write_license(doc, out)
check("license 文件已落盘", out.is_file(), str(out))
# ---- 7) 查看/校验 ----
vt = w.inspect_tab
vt.lic_edit.setText(str(out))
vt.pub_edit.setText(w.signing_key.public_b64)
vt._show_only()
check("仅查看不验签也能看到编号", payload["lic_id"] in vt.detail.text())
vt.rb_remote.setChecked(True)
vt.rc_edit.setText(rc)
vt._run()
check("目标机器校验通过", "✓" in vt.verdict.text(), vt.verdict.text())
other = {k: ("ffffffff" if k == "mac" else v) for k, v in src.items()}
other["machine_id"] = "anothervalueanothervalue"
vt.rc_edit.setText(encode_request_code(other))
vt._run()
check("异机校验被拒绝", "✗" in vt.verdict.text(), vt.verdict.text())
# 无公钥时应提示而不是静默失败
vt.pub_edit.setText("")
before = len(dialogs)
vt._run()
check("缺公钥时给出提示", len(dialogs) > before, str(dialogs[-1] if dialogs else None))
# ---- 8) 有效期三种模式 ----
it.rb_forever.setChecked(True)
it._refresh_preview()
check("永久模式 expires_at 为 None",
json.loads(it.preview.toPlainText())["expires_at"] is None)
it.rb_days.setChecked(True)
it._refresh_preview()
days_value = json.loads(it.preview.toPlainText())["expires_at"]
check("N 天模式给出到期日", bool(days_value), str(days_value))
it.rb_date.setChecked(True)
it._refresh_preview()
check("指定日期模式生效",
json.loads(it.preview.toPlainText())["expires_at"]
== it.date_edit.date().toString("yyyy-MM-dd"))
check("不勾选时对应输入框被禁用",
it.days_spin.isEnabled() is False and it.date_edit.isEnabled() is True)
# ---- 9) 台账页:签发即入库、检索、续期、导出 ----
rt = w.registry_tab
db = tmp / "licenses.db"
rt.db_edit.setText(str(db))
rt._open_db(silent=True)
check("台账库已打开", w.registry.is_open and w.registry.path == db, str(w.registry.path))
check("新库为空", rt.table.rowCount() == 0, str(rt.table.rowCount()))
# 签发页的"签发即入库"是台账的数据来源,这里直接调 holder 模拟该动作
check("签发页默认勾选「记入台账」", it.record_check.isChecked())
w.registry.save(doc, public_key_fp=w.signing_key.fingerprint, source="smoke")
rt.refresh()
check("入库后台账可见", rt.table.rowCount() == 1, str(rt.table.rowCount()))
row0 = [rt.table.item(0, c).text() for c in range(9)]
check("台账显示客户", row0[1] == "GUI 测试客户", row0[1])
check("台账显示机器码", row0[2] == it._machine_code, row0[2])
check("台账显示状态与剩余天数", "即将到期" in row0[4] and "d)" in row0[4], row0[4])
check("台账显示备注", row0[7] == "合同备注", row0[7])
# 筛选
rt.machine_edit.setText(it._machine_code[:12])
rt.refresh()
check("按机器码筛选命中", rt.table.rowCount() == 1, str(rt.table.rowCount()))
rt.machine_edit.setText("ZZZZ-NOT-EXIST")
rt.refresh()
check("筛选无结果时为空表", rt.table.rowCount() == 0, str(rt.table.rowCount()))
rt._reset_filters()
check("重置后恢复全量", rt.table.rowCount() == 1, str(rt.table.rowCount()))
# 续期对话框:默认生成新编号、复用原指纹
from chongming_license_gui.registry_tab import RenewDialog
rec = rt.records[0]
dlg = RenewDialog(rec)
check("续期默认生成新编号(新旧都留台账)", dlg.values()["keep_lic_id"] is False)
dlg.rb_keep_id.setChecked(True)
check("可切换为沿用原编号", dlg.values()["keep_lic_id"] is True)
dlg.rb_keep_id.setChecked(False)
dlg.days_spin.setValue(365)
vals = dlg.values()
check("续期预填原插件", vals["plugins"] == rec.plugins, str(vals["plugins"]))
check("续期复用原指纹(无需再要申请码)", rec.sources == it._sources)
# 执行续期(走与界面相同的构造路径)
from chongming_license import build_license as _bl, build_payload as _bp, write_license as _wl
np = _bp(machine_code=rec.machine_code, plugins=vals["plugins"], sources=rec.sources,
customer=vals["customer"], note=vals["note"], expires_at=vals["expires_at"])
ndoc = _bl(np, w.signing_key.key, kid=rec.kid)
_wl(ndoc, tmp / "renewed.lic")
new_rec = w.registry.save(ndoc, public_key_fp=w.signing_key.fingerprint, source="renew")
reg = w.registry.ensure_open()
reg.update_note(rec.lic_id, f"{rec.note};已续期 → {new_rec.lic_id}")
rt.refresh()
check("续期后新旧记录都在台账", rt.table.rowCount() == 2, str(rt.table.rowCount()))
check("新授权为有效状态", new_rec.status() == "active", new_rec.status())
check("旧记录留下续期链",
f"已续期 → {new_rec.lic_id}" in reg.get(rec.lic_id).note,
reg.get(rec.lic_id).note)
check("续期后文件可独立验签",
read_license(tmp / "renewed.lic", w.signing_key.public_b64,
sources=it._sources).ok)
# 编辑备注:人工批注不被重新导入覆盖
reg.update_note(new_rec.lic_id, "人工批注")
reg.save(ndoc, public_key_fp=w.signing_key.fingerprint)
check("重新入库不覆盖人工批注",
reg.get(new_rec.lic_id).note == "人工批注", reg.get(new_rec.lic_id).note)
# 导出
files = reg.export_licenses(tmp / "archive", rt.records, subdir_by_customer=True)
check("导出 .lic 数量正确", len(files) == 2, str([f.name for f in files]))
csv_path = reg.export_csv(tmp / "台账.csv", rt.records)
check("CSV 含表头 + 数据行",
len(csv_path.read_text(encoding="utf-8-sig").splitlines()) == 3)
check("CSV 为 UTF-8 BOM(Excel 中文不乱码)",
csv_path.read_bytes()[:3] == b"\xef\xbb\xbf")
# 删除
check("删除记录", reg.delete(new_rec.lic_id) is True)
rt.refresh()
check("删除后台账少一条", rt.table.rowCount() == 1, str(rt.table.rowCount()))
print("\n" + "=" * 56)
if failures:
print(f"❌ {len(failures)} 项未通过: {failures}")
return 1
print("✅ GUI 冒烟测试全部通过")
return 0
if __name__ == "__main__":
sys.exit(main())