import base64 import json from fastapi.testclient import TestClient from sqlalchemy import select from memrelay.errors import AppError from memrelay.models import CredentialMapping, VaultConnection from memrelay.vault_service import VaultService from tests.conftest import csrf_headers class FakeBwRunner: def __init__(self) -> None: self.fail_sync = False self.commands: list[list[str]] = [] self.items = [ { "id": "item-1", "name": "Git Service", "login": { "username": "developer", "password": "git-password", "totp": "JBSWY3DPEHPK3PXP", "uris": [{"uri": "https://git.example.com"}], }, "fields": [{"name": "api_token", "value": "token-value"}], "notes": "private note", }, { "id": "item-2", "name": "Git Mirror", "login": { "username": "mirror", "password": "mirror-password", "uris": [{"uri": "https://mirror.example.com"}], }, "fields": [], }, ] async def __call__(self, arguments: list[str], environment: dict[str, str]) -> str: self.commands.append(arguments) if arguments[0] in {"logout", "config"}: return "" if arguments[:2] == ["login", "--apikey"]: assert environment == { "BW_CLIENTID": "user.api-client-id", "BW_CLIENTSECRET": "api-client-secret", } return "" if arguments[0] in {"login", "unlock"}: assert environment["BW_PASSWORD"] == "vault-password" return "session-token" if arguments[0] == "sync": if self.fail_sync: raise AppError("VAULT_UNAVAILABLE", "密码库服务不可达", 503) return "" if arguments[:2] == ["list", "items"]: if "--search" not in arguments: return json.dumps(self.items) query = arguments[arguments.index("--search") + 1].casefold() matches = [ item for item in self.items if query in item["name"].casefold() or any( query in str(uri.get("uri", "")).casefold() for uri in (item.get("login") or {}).get("uris", []) ) ] return json.dumps(matches) if arguments[:2] == ["get", "item"]: item_id = arguments[2] return json.dumps(next(item for item in self.items if item["id"] == item_id)) if arguments[:2] == ["get", "totp"]: return "123456" if arguments[:2] == ["create", "item"]: item = json.loads(base64.b64decode(arguments[2]).decode()) item["id"] = f"item-{len(self.items) + 1}" item["revisionDate"] = "2026-08-03T00:00:00Z" self.items.append(item) return json.dumps(item) if arguments[:2] == ["edit", "item"]: item_id = arguments[2] item = json.loads(base64.b64decode(arguments[3]).decode()) item["id"] = item_id item["revisionDate"] = "2026-08-03T00:01:00Z" index = next( index for index, current in enumerate(self.items) if current["id"] == item_id ) self.items[index] = item return json.dumps(item) if arguments[:2] == ["delete", "item"]: item_id = arguments[2] self.items = [item for item in self.items if item["id"] != item_id] return "" raise AssertionError(f"Unexpected bw command: {arguments}") def install_fake_vault(client: TestClient, runner: FakeBwRunner) -> None: client.app.state.vault = VaultService( client.app.state.settings, client.app.state.session_factory, client.app.state.secret_box, runner, ) def configure_vault(client: TestClient) -> dict: response = client.put( "/api/v1/vault/connection", json={ "server_url": "https://vault.example.com", "account": "user@example.com", "password": "vault-password", }, headers=csrf_headers(client), ) assert response.status_code == 200, response.text return response.json() def configure_vault_with_api_key(client: TestClient) -> dict: response = client.put( "/api/v1/vault/connection", json={ "server_url": "https://vault.example.com", "login_method": "api_key", "client_id": "user.api-client-id", "client_secret": "api-client-secret", "password": "vault-password", }, headers=csrf_headers(client), ) assert response.status_code == 200, response.text return response.json() def test_configure_encrypt_sync_and_disconnect(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configured = configure_vault(client) assert configured["status"] == "unlocked" assert configured["last_sync_at"] is not None with client.app.state.session_factory() as db: connection = db.get(VaultConnection, 1) assert connection is not None assert "vault-password" not in connection.encrypted_password assert "user@example.com" not in connection.encrypted_account assert client.delete("/api/v1/vault/connection").status_code == 403 assert ( client.delete("/api/v1/vault/connection", headers=csrf_headers(client)).status_code == 204 ) assert client.get("/api/v1/vault/status").json()["configured"] is False def test_configure_with_api_key_encrypts_and_unlocks(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configured = configure_vault_with_api_key(client) assert configured["login_method"] == "api_key" assert configured["account"] == "user.api-client-id" assert ["login", "--apikey"] in runner.commands assert ["unlock", "--passwordenv", "BW_PASSWORD", "--raw"] in runner.commands with client.app.state.session_factory() as db: connection = db.get(VaultConnection, 1) assert connection is not None assert connection.login_method == "api_key" assert "api-client-secret" not in (connection.encrypted_client_secret or "") assert "vault-password" not in connection.encrypted_password def test_vault_login_fields_are_validated(initialized_client: TestClient) -> None: client = initialized_client install_fake_vault(client, FakeBwRunner()) display_name = client.put( "/api/v1/vault/connection", json={ "server_url": "https://vault.example.com", "account": "display-name", "password": "vault-password", }, headers=csrf_headers(client), ) missing_api_secret = client.put( "/api/v1/vault/connection", json={ "server_url": "https://vault.example.com", "login_method": "api_key", "client_id": "user.api-client-id", "password": "vault-password", }, headers=csrf_headers(client), ) assert display_name.status_code == 422 assert missing_api_secret.status_code == 422 def test_search_resolve_secret_fields_totp_and_mapping(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configure_vault(client) search = client.get("/api/v1/vault/secrets", params={"query": "Git"}).json() assert len(search["results"]) == 2 assert "password" not in json.dumps(search) ambiguous = client.post( "/api/v1/vault/secrets/resolve", json={"lookup_key": "Git"}, headers=csrf_headers(client), ) assert ambiguous.status_code == 409 assert ambiguous.json()["error"]["code"] == "SECRET_AMBIGUOUS" selected = client.post( "/api/v1/vault/secrets/resolve", json={"lookup_key": "Git", "item_id": "item-1"}, headers=csrf_headers(client), ) assert selected.json()["id"] == "item-1" password = client.post( "/api/v1/vault/secrets/value", json={"lookup_key": "Git", "field": "password"}, ) assert password.json()["value"] == "git-password" assert password.headers["cache-control"] == "no-store" custom = client.post( "/api/v1/vault/secrets/value", json={"lookup_key": "Git", "field": "api_token"}, ) assert custom.json()["value"] == "token-value" totp = client.post("/api/v1/vault/secrets/totp", json={"lookup_key": "Git"}) assert totp.json()["value"] == "123456" def test_list_get_create_update_and_delete_items(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configure_vault(client) list_commands_after_sync = runner.commands.count(["list", "items"]) listed = client.get("/api/v1/vault/items") assert listed.status_code == 200 assert len(listed.json()["results"]) == 2 assert "git-password" not in listed.text detail = client.get("/api/v1/vault/items/item-1") assert detail.status_code == 200 assert detail.headers["cache-control"] == "no-store" assert detail.json()["password"] == "git-password" assert runner.commands.count(["list", "items"]) == list_commands_after_sync assert ["get", "item", "item-1"] not in runner.commands payload = { "name": "Package Registry", "username": "builder", "password": "registry-password", "uris": ["https://packages.example.com"], "notes": "Used by release jobs", "totp": "otpauth://totp/example", "fields": [ {"name": "access_token", "value": "token-value", "hidden": True}, {"name": "environment", "value": "production", "hidden": False}, ], "lookup_key": "registry", } assert client.post("/api/v1/vault/items", json=payload).status_code == 403 created = client.post("/api/v1/vault/items", json=payload, headers=csrf_headers(client)) assert created.status_code == 200, created.text assert created.json()["created"] is True item_id = created.json()["item"]["id"] assert created.json()["item"]["fields"][0]["hidden"] is True with client.app.state.session_factory() as db: mapping = db.scalar( select(CredentialMapping).where(CredentialMapping.lookup_key == "registry") ) assert mapping is not None assert mapping.vault_item_id == item_id payload["password"] = "updated-password" updated_by_uri = client.post("/api/v1/vault/items", json=payload, headers=csrf_headers(client)) assert updated_by_uri.status_code == 200 assert updated_by_uri.json()["created"] is False assert updated_by_uri.json()["item"]["id"] == item_id assert len(runner.items) == 3 payload["name"] = "Renamed Registry" updated_by_id = client.put( f"/api/v1/vault/items/{item_id}", json=payload, headers=csrf_headers(client) ) assert updated_by_id.status_code == 200 assert updated_by_id.json()["item"]["name"] == "Renamed Registry" assert client.delete(f"/api/v1/vault/items/{item_id}").status_code == 403 deleted = client.delete(f"/api/v1/vault/items/{item_id}", headers=csrf_headers(client)) assert deleted.status_code == 204 assert all(item["id"] != item_id for item in runner.items) with client.app.state.session_factory() as db: assert ( db.scalar(select(CredentialMapping).where(CredentialMapping.lookup_key == "registry")) is None ) def test_automatic_save_rejects_ambiguous_exact_matches(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() runner.items[1]["name"] = "Git Service" install_fake_vault(client, runner) configure_vault(client) response = client.post( "/api/v1/vault/items", json={"name": "Git Service", "password": "replacement"}, headers=csrf_headers(client), ) assert response.status_code == 409 assert response.json()["error"]["code"] == "SECRET_AMBIGUOUS" def test_automatic_save_uses_name_and_provided_identity_fields( initialized_client: TestClient, ) -> None: client = initialized_client runner = FakeBwRunner() runner.items[1]["name"] = "Git Service" install_fake_vault(client, runner) configure_vault(client) matched = client.post( "/api/v1/vault/items", json={ "name": "Git Service", "username": "mirror", "uris": ["https://mirror.example.com/"], "password": "rotated-mirror-password", }, headers=csrf_headers(client), ) assert matched.status_code == 200, matched.text assert matched.json()["created"] is False assert matched.json()["item"]["id"] == "item-2" distinct_account = client.post( "/api/v1/vault/items", json={ "name": "Git Service", "username": "release-bot", "uris": ["https://git.example.com"], "password": "release-password", }, headers=csrf_headers(client), ) assert distinct_account.status_code == 200, distinct_account.text assert distinct_account.json()["created"] is True renamed_same_uri = client.post( "/api/v1/vault/items", json={ "name": "Git Production", "username": "developer", "uris": ["https://git.example.com"], "password": "production-password", }, headers=csrf_headers(client), ) assert renamed_same_uri.status_code == 200, renamed_same_uri.text assert renamed_same_uri.json()["created"] is True def test_partial_automatic_update_preserves_unspecified_fields( initialized_client: TestClient, ) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configure_vault(client) response = client.post( "/api/v1/vault/items", json={"name": "Git Service", "password": "rotated-password"}, headers=csrf_headers(client), ) assert response.status_code == 200 saved = response.json()["item"] assert saved["password"] == "rotated-password" assert saved["username"] == "developer" assert saved["uris"] == ["https://git.example.com"] assert saved["notes"] == "private note" assert saved["totp"] == "JBSWY3DPEHPK3PXP" assert saved["fields"] == [{"name": "api_token", "value": "token-value", "hidden": False}] def test_unique_resolution_offline_cache_and_restart_unlock(initialized_client: TestClient) -> None: client = initialized_client runner = FakeBwRunner() install_fake_vault(client, runner) configure_vault(client) unique = client.post( "/api/v1/vault/secrets/resolve", json={"lookup_key": "Mirror"}, headers=csrf_headers(client), ) assert unique.json()["id"] == "item-2" runner.fail_sync = True failed = client.post("/api/v1/vault/sync", headers=csrf_headers(client)) assert failed.status_code == 503 assert client.get("/api/v1/vault/status").json()["status"] == "cached" cached = client.get("/api/v1/vault/secrets", params={"query": "Mirror"}).json() assert cached["cached"] is True restarted_runner = FakeBwRunner() restarted = VaultService( client.app.state.settings, client.app.state.session_factory, client.app.state.secret_box, restarted_runner, ) client.app.state.vault = restarted value = client.post( "/api/v1/vault/secrets/value", json={"lookup_key": "Mirror", "field": "username"}, ) assert value.json()["value"] == "mirror" assert any(command[0] == "unlock" for command in restarted_runner.commands) assert not any(command[0] == "config" for command in restarted_runner.commands) def test_vault_accepts_private_http_and_rejects_invalid_schemes( initialized_client: TestClient, ) -> None: client = initialized_client install_fake_vault(client, FakeBwRunner()) configured = client.put( "/api/v1/vault/connection", json={ "server_url": "http://vault.local", "account": "user@example.com", "password": "vault-password", }, headers=csrf_headers(client), ) assert configured.status_code == 200, configured.text assert configured.json()["server_url"] == "http://vault.local" invalid = client.put( "/api/v1/vault/connection", json={ "server_url": "ftp://vault.local", "account": "user@example.com", "password": "vault-password", }, headers=csrf_headers(client), ) assert invalid.status_code == 422 assert invalid.json()["error"]["code"] == "VAULT_URL_INVALID"