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