"""密码本与凭据:加解密、占位符、按域名分组(模型看不到明文)、profile 路径与 Electron 分区名。""" from __future__ import annotations import pytest from backend import security from backend.browser import runner from backend.orchestrator import orchestrator as orc from backend.orchestrator.orchestrator import Orchestrator @pytest.fixture def enc_key(monkeypatch): from backend.config import settings monkeypatch.setattr(settings, "account_enc_key", security.generate_key()) class TestSecretEncryption: def test_round_trip(self, enc_key): assert security.decrypt_secret(security.encrypt_secret("p@ss 密码 123")) == "p@ss 密码 123" def test_ciphertext_does_not_contain_the_plaintext(self, enc_key): assert "p@ssw0rd" not in security.encrypt_secret("p@ssw0rd") def test_same_plaintext_encrypts_differently_each_time(self, enc_key): assert security.encrypt_secret("same") != security.encrypt_secret("same") def test_wrong_key_gives_a_readable_error(self, enc_key, monkeypatch): from backend.config import settings token = security.encrypt_secret("secret") monkeypatch.setattr(settings, "account_enc_key", security.generate_key()) with pytest.raises(RuntimeError, match="ACCOUNT_ENC_KEY"): security.decrypt_secret(token) def test_missing_key_tells_the_operator_what_to_do(self, monkeypatch): from backend.config import settings monkeypatch.setattr(settings, "account_enc_key", "") with pytest.raises(RuntimeError, match="ACCOUNT_ENC_KEY"): security.encrypt_secret("x") class TestPlaceholders: def test_placeholders_carry_the_system_id(self): assert orc._placeholders("xgxt") == ("x_username_xgxt", "x_password_xgxt") def test_two_systems_never_share_a_placeholder(self): assert orc._placeholders("xgxt") != orc._placeholders("lib") class TestSensitiveData: @staticmethod def _account(username: str, password: str) -> dict: return {"username": username, "password_enc": security.encrypt_secret(password), "role": "辅导员"} def test_grouped_by_domain_with_system_suffixed_placeholders(self, knowledge, enc_key): systems = knowledge["systems"] accounts = {"xgxt": self._account("t001", "pw-xgxt"), "lib": self._account("t002", "pw-lib")} data = Orchestrator._sensitive_data(systems, accounts) assert set(data) == {"example.edu", "*.example.edu", "lib.example.edu"} assert data["example.edu"] == {"x_username_xgxt": "t001", "x_password_xgxt": "pw-xgxt"} assert data["lib.example.edu"] == {"x_username_lib": "t002", "x_password_lib": "pw-lib"} def test_a_systems_password_never_reaches_another_systems_domain(self, knowledge, enc_key): accounts = {"xgxt": self._account("t001", "pw-xgxt"), "lib": self._account("t002", "pw-lib")} data = Orchestrator._sensitive_data(knowledge["systems"], accounts) assert "pw-lib" not in str(data["example.edu"]) assert "pw-xgxt" not in str(data["lib.example.edu"]) def test_systems_without_an_account_are_skipped(self, knowledge, enc_key): data = Orchestrator._sensitive_data(knowledge["systems"], {"xgxt": self._account("t001", "pw")}) assert "lib.example.edu" not in data def test_no_accounts_at_all_returns_none(self, knowledge): # 返回 None 而不是空字典:browser-use 收到空字典也会进敏感数据分支 assert Orchestrator._sensitive_data(knowledge["systems"], {"xgxt": None, "lib": None}) is None def test_system_without_allowed_domains_falls_back_to_ungrouped(self, enc_key): systems = [{"id": "sso", "name": "统一认证", "allowed_domains": []}] data = Orchestrator._sensitive_data(systems, {"sso": self._account("u", "p")}) assert data == {"x_username_sso": "u", "x_password_sso": "p"} def test_password_is_decrypted_only_here(self, knowledge, enc_key): account = self._account("t001", "明文密码") assert "明文密码" not in account["password_enc"] data = Orchestrator._sensitive_data(knowledge["systems"], {"xgxt": account}) assert data["example.edu"]["x_password_xgxt"] == "明文密码" class TestProfilePath: def test_separates_by_person_system_and_role(self): a = orc._profile_path("s001", "xgxt", "辅导员") b = orc._profile_path("s001", "xgxt", "学生") c = orc._profile_path("s002", "xgxt", "辅导员") assert a != b != c and a != c def test_missing_role_still_produces_a_path(self): assert orc._profile_path("s001", "xgxt", None) def test_same_inputs_give_the_same_path(self): assert orc._profile_path("s001", "xgxt", "辅导员") == orc._profile_path("s001", "xgxt", "辅导员") class TestElectronPartition: def test_partition_is_ascii_safe(self): name = runner._partition_for(orc._profile_path("s001", "xgxt", "辅导员")) assert name.isascii() and name.startswith("sa-") def test_different_profiles_get_different_partitions(self): a = runner._partition_for(orc._profile_path("s001", "xgxt", "辅导员")) b = runner._partition_for(orc._profile_path("s001", "xgxt", "学生")) assert a != b def test_chinese_only_paths_still_differ(self): # 角色名是中文,去掉非 ASCII 字符后前缀可能相同,靠哈希后缀区分 a = runner._partition_for("profiles/张三/学工_辅导员") b = runner._partition_for("profiles/张三/学工_学生") assert a != b and a.isascii() and b.isascii() def test_partition_is_stable(self): path = orc._profile_path("s001", "xgxt", "辅导员") assert runner._partition_for(path) == runner._partition_for(path)