assistant/tests/test_credentials.py

127 lines
5.7 KiB
Python
Raw Permalink 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.

"""密码本与凭据:加解密、占位符、按域名分组(模型看不到明文)、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)