first commit
This commit is contained in:
+284
@@ -0,0 +1,284 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user