feat: 完成T-503敏感信息提示与脱敏
- 保存或变更账号密码、AI API Key 前弹出本地明文保存提示 - 新增敏感值打码、结构化日志脱敏和自由文本替换工具 - 补充 GUI/appconfig 单测,覆盖明文提示和脱敏边界 - 同步任务看板、当前状态、架构、API、路由和编码规则文档
This commit is contained in:
@@ -113,6 +113,30 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_sanitize_for_log_masks_secret_fields(self):
|
||||
payload = {
|
||||
"name": "demo",
|
||||
"api_key": "sk-1234567890",
|
||||
"nested": {
|
||||
"password": "account-secret",
|
||||
"items": [
|
||||
{"provider_token": "token-secret"},
|
||||
{"value": "safe"},
|
||||
{"api_key": {"value": "nested-secret"}},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
sanitized = appconfig.sanitize_for_log(payload)
|
||||
|
||||
self.assertEqual("demo", sanitized["name"])
|
||||
self.assertEqual("sk-1***7890", sanitized["api_key"])
|
||||
self.assertEqual("acco***cret", sanitized["nested"]["password"])
|
||||
self.assertEqual("toke***cret", sanitized["nested"]["items"][0]["provider_token"])
|
||||
self.assertEqual("safe", sanitized["nested"]["items"][1]["value"])
|
||||
self.assertEqual("***", sanitized["nested"]["items"][2]["api_key"])
|
||||
self.assertEqual("sk-1234567890", payload["api_key"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+75
-1
@@ -179,7 +179,12 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
tab.api_key_edit.setText("sk-custom-secret")
|
||||
tab.api_type_combo.setCurrentIndex(tab.api_type_combo.findData("chat"))
|
||||
tab.connect_timeout_spin.setValue(12)
|
||||
tab.save_model()
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning:
|
||||
tab.save_model()
|
||||
|
||||
warning.assert_called_once()
|
||||
self.assertIn("本地明文保存", warning.call_args[0][1])
|
||||
self.assertIn("config/ai_models.json", warning.call_args[0][2])
|
||||
|
||||
saved = appconfig.get_model("Text Custom", path=models_path)
|
||||
self.assertEqual("text", saved["category"])
|
||||
@@ -187,6 +192,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertEqual("sk-custom-secret", saved["api_key"])
|
||||
self.assertEqual(12, saved["connect_timeout_seconds"])
|
||||
self.assertIn("AI 模型已保存:Text Custom", statuses[-1])
|
||||
self.assertNotIn("sk-custom-secret", statuses[-1])
|
||||
self.assertTrue(tab.delete_model_button.isEnabled())
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.question", return_value=gui.QMessageBox.Yes):
|
||||
@@ -444,6 +450,29 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_ai_model_test_worker_sanitizes_secret_payload_fields(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
models_path = os.path.join(temp_dir, "ai_models.json")
|
||||
worker = AIModelTestWorker("Text A", ai_models_path=models_path)
|
||||
|
||||
with mock.patch(
|
||||
"app.gui.appconfig.test_ai_model",
|
||||
return_value={
|
||||
"ok": False,
|
||||
"api_key": "sk-worker-secret",
|
||||
"password": "worker-password",
|
||||
"error": "连接失败",
|
||||
},
|
||||
):
|
||||
result = worker.execute()
|
||||
|
||||
self.assertEqual("Text A", result["name"])
|
||||
self.assertEqual("sk-w***cret", result["api_key"])
|
||||
self.assertEqual("work***word", result["password"])
|
||||
self.assertEqual("连接失败", result["error"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_generate_tab_has_prompt_editors_and_task_table(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
title_prompt_path = os.path.join(temp_dir, "title_prompt.txt")
|
||||
@@ -1343,6 +1372,51 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_accounts_tab_warns_before_saving_plaintext_password(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
statuses = []
|
||||
tab = AccountsTab(config=cfg, status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
class FakeDialog:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def exec(self):
|
||||
return gui.QDialog.Accepted
|
||||
|
||||
def values(self):
|
||||
return {
|
||||
"account_name": "主店",
|
||||
"alias": "alias",
|
||||
"region_host": "seller.shopee.tw",
|
||||
"debug_port": 9222,
|
||||
"password": "plain-password",
|
||||
"note": "",
|
||||
}
|
||||
|
||||
with mock.patch("app.gui.AccountDialog", FakeDialog), mock.patch(
|
||||
"app.gui.QMessageBox.warning"
|
||||
) as warning:
|
||||
tab.add_account()
|
||||
|
||||
warning.assert_called_once()
|
||||
self.assertIn("本地明文保存", warning.call_args[0][1])
|
||||
self.assertIn("SQLite", warning.call_args[0][2])
|
||||
stored = accounts.get_account("alias", config=cfg)
|
||||
self.assertEqual("plain-password", stored.password)
|
||||
visible_values = [
|
||||
tab.table.item(0, column).text()
|
||||
for column in range(tab.table.columnCount())
|
||||
if tab.table.item(0, column) is not None
|
||||
]
|
||||
self.assertNotIn("plain-password", visible_values)
|
||||
self.assertIn("账号已新增", statuses[-1])
|
||||
self.assertNotIn("plain-password", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_accounts_tab_updates_login_status_cell(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user