from types import SimpleNamespace from pathlib import Path import os import base64 import json import pytest from seedpass import api import seedpass.core.agent_export_policy as export_policy import seedpass.core.agent_approval as approval_core import seedpass.core.agent_secret_isolation as isolation_core import seedpass.core.agent_secret_lease as lease_core import seedpass.core.agent_job as job_core import seedpass.core.agent_recovery as recovery_core import string from seedpass.core.entry_types import EntryType from seedpass.core.password_generation import PasswordGenerator, PasswordPolicy from seedpass.core.encryption import EncryptionManager from nostr.client import NostrClient, DEFAULT_RELAYS @pytest.mark.anyio async def test_create_and_modify_totp_entry(client): cl, token = client calls = {} def add_totp(label, seed, **kwargs): calls["create"] = kwargs return "uri" def modify(idx, **kwargs): calls["modify"] = (idx, kwargs) api.app.state.pm.entry_manager.add_totp = add_totp api.app.state.pm.entry_manager.modify_entry = modify api.app.state.pm.entry_manager.get_next_index = lambda: 5 api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/entry", json={ "type": "totp", "label": "T", "index": 1, "secret": "abc", "period": 60, "digits": 8, "notes": "n", }, headers=headers, ) assert res.status_code == 200 assert res.json() == {"id": 5, "uri": "uri"} assert calls["create"] == { "index": 1, "secret": "abc", "period": 60, "digits": 8, "notes": "n", "archived": False, "deterministic": False, } res = await cl.put( "/api/v1/entry/5", json={"period": 90, "digits": 6}, headers=headers, ) assert res.status_code == 200 assert calls["modify"][0] == 5 assert calls["modify"][1]["period"] == 90 assert calls["modify"][1]["digits"] == 6 @pytest.mark.anyio async def test_create_and_modify_ssh_entry(client): cl, token = client calls = {} def add_ssh(label, seed, **kwargs): calls["create"] = kwargs return 2 def modify(idx, **kwargs): calls["modify"] = (idx, kwargs) api.app.state.pm.entry_manager.add_ssh_key = add_ssh api.app.state.pm.entry_manager.modify_entry = modify api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/entry", json={"type": "ssh", "label": "S", "index": 2, "notes": "n"}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"id": 2} assert calls["create"] == {"index": 2, "notes": "n", "archived": False} res = await cl.put( "/api/v1/entry/2", json={"notes": "x"}, headers=headers, ) assert res.status_code == 200 assert calls["modify"][0] == 2 assert calls["modify"][1]["notes"] == "x" @pytest.mark.anyio async def test_create_and_modify_document_entry(client): cl, token = client calls = {} def add_document(label, content, **kwargs): calls["create"] = (label, content, kwargs) return 11 def modify(idx, **kwargs): calls["modify"] = (idx, kwargs) api.app.state.pm.entry_manager.add_document = add_document api.app.state.pm.entry_manager.modify_entry = modify headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/entry", json={ "type": "document", "title": "Runbook", "content": "line1\\nline2", "file_type": "md", "notes": "ops", "tags": ["doc", "ops"], }, headers=headers, ) assert res.status_code == 200 assert res.json() == {"id": 11} assert calls["create"] == ( "Runbook", "line1\\nline2", {"file_type": "md", "notes": "ops", "archived": False, "tags": ["doc", "ops"]}, ) res = await cl.put( "/api/v1/entry/11", json={"content": "line1\\nline2\\nline3", "file_type": "txt"}, headers=headers, ) assert res.status_code == 200 assert calls["modify"][0] == 11 assert calls["modify"][1]["content"].endswith("line3") assert calls["modify"][1]["file_type"] == "txt" @pytest.mark.anyio async def test_document_import_export_endpoints(client): cl, token = client calls = {} def import_document(path, **kwargs): calls["import"] = (path, kwargs) return 33 def export_document(idx, **kwargs): calls["export"] = (idx, kwargs) return Path("/tmp/exported.md") api.app.state.pm.entry_manager.import_document_file = import_document api.app.state.pm.entry_manager.export_document_file = export_document headers = {"Authorization": f"Bearer {token}"} imported = await cl.post( "/api/v1/entry/document/import", json={"path": "/tmp/source.md", "label": "Imported", "notes": "n"}, headers=headers, ) assert imported.status_code == 200 assert imported.json() == {"id": 33} assert calls["import"] == ( "/tmp/source.md", {"label": "Imported", "notes": "n", "archived": False, "tags": None}, ) exported = await cl.post( "/api/v1/entry/33/document/export", json={"path": "/tmp/out", "overwrite": True}, headers=headers, ) assert exported.status_code == 200 assert exported.json()["path"] in ["/tmp/exported.md", "\\tmp\\exported.md"] assert calls["export"] == (33, {"output_path": "/tmp/out", "overwrite": True}) @pytest.mark.anyio async def test_entry_links_endpoints(client): cl, token = client calls = {} def add_link(entry_id, target_id, **kwargs): calls["add"] = (entry_id, target_id, kwargs) return [ { "target_id": target_id, "relation": kwargs["relation"], "note": kwargs["note"], } ] def remove_link(entry_id, target_id, **kwargs): calls["remove"] = (entry_id, target_id, kwargs) return [] def get_links(entry_id): calls["get"] = entry_id return [ { "target_id": 2, "relation": "references", "note": "used by doc", "target_label": "API Token", "target_kind": "key_value", } ] api.app.state.pm.entry_manager.add_link = add_link api.app.state.pm.entry_manager.remove_link = remove_link api.app.state.pm.entry_manager.get_links = get_links headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/entry/1/links", json={"target_id": 2, "relation": "references", "note": "used by doc"}, headers=headers, ) assert created.status_code == 200 assert created.json()["links"][0]["target_id"] == 2 assert calls["add"] == (1, 2, {"relation": "references", "note": "used by doc"}) listed = await cl.get("/api/v1/entry/1/links", headers=headers) assert listed.status_code == 200 assert listed.json()["entry_id"] == 1 assert listed.json()["links"][0]["target_kind"] == "key_value" assert calls["get"] == 1 removed = await cl.request( "DELETE", "/api/v1/entry/1/links", json={"target_id": 2, "relation": "references"}, headers=headers, ) assert removed.status_code == 200 assert removed.json() == {"entry_id": 1, "links": []} assert calls["remove"] == (1, 2, {"relation": "references"}) @pytest.mark.anyio async def test_update_entry_error(client): cl, token = client def modify(*a, **k): raise ValueError("nope") api.app.state.pm.entry_manager.modify_entry = modify headers = {"Authorization": f"Bearer {token}"} res = await cl.put("/api/v1/entry/1", json={"username": "x"}, headers=headers) assert res.status_code == 400 assert res.json() == {"detail": "nope"} @pytest.mark.anyio async def test_get_entry_private_kind_blocked_when_high_risk_locked( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(isolation_core, "APP_DIR", tmp_path) isolation_core.set_high_risk_factor("factor-get") (tmp_path / "agent_policy.json").write_text( json.dumps({"secret_isolation": {"enabled": True, "high_risk_kinds": ["ssh"]}}), encoding="utf-8", ) api.app.state.pm.current_fingerprint = "ABC123" api.app.state.pm.entry_manager.retrieve_entry = lambda _i: { "kind": "ssh", "label": "SSH key", } headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } blocked = await cl.get("/api/v1/entry/1", headers=headers) assert blocked.status_code == 403 assert blocked.json()["detail"] == "policy_deny:high_risk_locked" unlock_headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-High-Risk-Factor": "factor-get", } unlocked = await cl.post( "/api/v1/high-risk/unlock", json={"ttl": 120}, headers=unlock_headers ) assert unlocked.status_code == 200 allowed = await cl.get("/api/v1/entry/1", headers=headers) assert allowed.status_code == 200 assert allowed.json()["kind"] == "ssh" @pytest.mark.anyio async def test_get_entry_hydrates_partitioned_high_risk_entry(client, monkeypatch): cl, token = client api.app.state.pm.current_fingerprint = "ABC123" api.app.state.pm.fingerprint_dir = "/tmp/seedpass-tests/fp" api.app.state.pm.entry_manager.retrieve_entry = lambda _i: { "kind": "ssh", "label": "SSH key", "partition": "high_risk", } monkeypatch.setattr(api, "high_risk_factor_configured", lambda: False) monkeypatch.setattr(api, "unlocked_partition_key_tag", lambda **kwargs: "tag-123") monkeypatch.setattr( api, "load_partition_entry", lambda fingerprint_dir, partition_key_tag, index: { "kind": "ssh", "label": "SSH key", "notes": "from-partition", }, ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } res = await cl.get("/api/v1/entry/1", headers=headers) assert res.status_code == 200 payload = res.json() assert payload["kind"] == "ssh" assert payload["notes"] == "from-partition" @pytest.mark.anyio async def test_update_config_secret_mode(client): cl, token = client called = {} def set_secret(val): called["val"] = val api.app.state.pm.config_manager.set_secret_mode_enabled = set_secret headers = {"Authorization": f"Bearer {token}"} res = await cl.put( "/api/v1/config/secret_mode_enabled", json={"value": True}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert called["val"] is True @pytest.mark.anyio async def test_totp_export_endpoint(client): cl, token = client api.app.state.pm.entry_manager.export_totp_entries = lambda seed: {"entries": ["x"]} api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw"} res = await cl.get("/api/v1/totp/export", headers=headers) assert res.status_code == 200 assert res.json() == {"entries": ["x"]} @pytest.mark.anyio async def test_totp_codes_endpoint(client): cl, token = client api.app.state.pm.entry_manager.list_entries = lambda **kw: [ (0, "Email", None, None, False) ] api.app.state.pm.entry_manager.get_totp_code = lambda i, s: "123456" api.app.state.pm.entry_manager.get_totp_time_remaining = lambda i: 30 api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw"} res = await cl.get("/api/v1/totp", headers=headers) assert res.status_code == 200 assert res.json() == { "codes": [ {"id": 0, "label": "Email", "code": "123456", "seconds_remaining": 30} ] } @pytest.mark.anyio async def test_parent_seed_endpoint_removed(client): cl, token = client res = await cl.get( "/api/v1/parent-seed", headers={"Authorization": f"Bearer {token}"} ) assert res.status_code == 404 @pytest.mark.anyio async def test_fingerprint_endpoints(client): cl, token = client calls = {} api.app.state.pm.add_new_fingerprint = lambda: calls.setdefault("add", True) api.app.state.pm.fingerprint_manager.remove_fingerprint = ( lambda fp: calls.setdefault("remove", fp) ) api.app.state.pm.select_fingerprint = lambda fp: calls.setdefault("select", fp) headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/fingerprint", headers=headers) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert calls.get("add") is True res = await cl.delete("/api/v1/fingerprint/abc", headers=headers) assert res.status_code == 200 assert res.json() == {"status": "deleted"} assert calls.get("remove") == "abc" res = await cl.post( "/api/v1/fingerprint/select", json={"fingerprint": "xyz"}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert calls.get("select") == "xyz" @pytest.mark.anyio async def test_checksum_endpoints(client): cl, token = client calls = {} api.app.state.pm.handle_verify_checksum = lambda: calls.setdefault("verify", True) api.app.state.pm.handle_update_script_checksum = lambda: calls.setdefault( "update", True ) headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/checksum/verify", headers=headers) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert calls.get("verify") is True res = await cl.post("/api/v1/checksum/update", headers=headers) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert calls.get("update") is True @pytest.mark.anyio async def test_vault_import_via_path(client, tmp_path): cl, token = client called = {} def import_db(path): called["path"] = path api.app.state.pm.handle_import_database = import_db api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True) api.app.state.pm.encryption_manager = SimpleNamespace( resolve_relative_path=lambda p: p ) file_path = tmp_path / "b.json.enc" file_path.write_text("{}") headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/vault/import", json={"path": str(file_path)}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert called["path"] == file_path assert called.get("sync") is True @pytest.mark.anyio async def test_vault_import_via_upload(client, tmp_path): cl, token = client called = {} def import_db(path): called["path"] = path api.app.state.pm.handle_import_database = import_db api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True) file_path = tmp_path / "c.json" file_path.write_text("{}") headers = {"Authorization": f"Bearer {token}"} with open(file_path, "rb") as fh: res = await cl.post( "/api/v1/vault/import", files={"file": ("c.json", fh.read())}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert isinstance(called.get("path"), Path) assert called.get("sync") is True @pytest.mark.anyio async def test_vault_import_upload_sets_secure_temp_permissions(client, monkeypatch): cl, token = client called = {} chmod_calls = [] unlink_calls = [] def import_db(path): called["path"] = path api.app.state.pm.handle_import_database = import_db api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True) real_chmod = api.os.chmod real_unlink = api.os.unlink def tracking_chmod(path, mode): chmod_calls.append((Path(path), mode)) return real_chmod(path, mode) def tracking_unlink(path): unlink_calls.append(Path(path)) return real_unlink(path) monkeypatch.setattr(api.os, "chmod", tracking_chmod) monkeypatch.setattr(api.os, "unlink", tracking_unlink) headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/vault/import", files={"file": ("c.json.enc", b"{}")}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} tmp_path_used = called["path"] assert any(path == tmp_path_used and mode == 0o600 for path, mode in chmod_calls) assert tmp_path_used in unlink_calls @pytest.mark.anyio async def test_vault_import_upload_temp_file_removed_on_failure(client, monkeypatch): cl, token = client called = {} unlink_calls = [] def import_db(path): called["path"] = path raise ValueError("boom") api.app.state.pm.handle_import_database = import_db api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True) real_unlink = api.os.unlink def tracking_unlink(path): unlink_calls.append(Path(path)) return real_unlink(path) monkeypatch.setattr(api.os, "unlink", tracking_unlink) headers = {"Authorization": f"Bearer {token}"} with pytest.raises(ValueError): await cl.post( "/api/v1/vault/import", files={"file": ("c.json.enc", b"{}")}, headers=headers, ) assert called["path"] in unlink_calls assert not called.get("sync", False) @pytest.mark.anyio async def test_vault_import_upload_too_large(client): cl, token = client called = {} api.app.state.pm.handle_import_database = lambda path: called.setdefault( "import_called", True ) api.app.state.pm.sync_vault = lambda: called.setdefault("sync_called", True) old_limit = api._MAX_IMPORT_BYTES api._MAX_IMPORT_BYTES = 8 try: headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/vault/import", files={"file": ("big.json.enc", b"123456789")}, headers=headers, ) finally: api._MAX_IMPORT_BYTES = old_limit assert res.status_code == 413 assert "exceeds max size" in res.json()["detail"] assert called == {} @pytest.mark.anyio async def test_vault_import_invalid_extension(client): cl, token = client api.app.state.pm.handle_import_database = lambda path: None api.app.state.pm.sync_vault = lambda: None api.app.state.pm.encryption_manager = SimpleNamespace( resolve_relative_path=lambda p: p ) headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/vault/import", json={"path": "bad.txt"}, headers=headers, ) assert res.status_code == 400 @pytest.mark.anyio async def test_vault_import_path_traversal_blocked(client, tmp_path): cl, token = client key = base64.urlsafe_b64encode(os.urandom(32)) api.app.state.pm.encryption_manager = EncryptionManager(key, tmp_path) api.app.state.pm.handle_import_database = lambda path: None api.app.state.pm.sync_vault = lambda: None headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/vault/import", json={"path": "../evil.json.enc"}, headers=headers, ) assert res.status_code == 400 @pytest.mark.anyio async def test_vault_lock_endpoint(client): cl, token = client called = {} def lock(): called["locked"] = True api.app.state.pm.locked = True api.app.state.pm.lock_vault = lock api.app.state.pm.locked = False headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/vault/lock", headers=headers) assert res.status_code == 200 assert res.json() == {"status": "locked"} assert called.get("locked") is True assert api.app.state.pm.locked is True api.app.state.pm.unlock_vault = lambda pw: setattr( api.app.state.pm, "locked", False ) api.app.state.pm.unlock_vault("pw") assert api.app.state.pm.locked is False @pytest.mark.anyio async def test_vault_unlock_endpoint(client): cl, token = client called = {} def unlock(pw): called["password"] = pw api.app.state.pm.locked = False api.app.state.pm.is_locked = False return 0.123 api.app.state.pm.verify_password = lambda pw: pw == "pw" api.app.state.pm.unlock_vault = unlock api.app.state.pm.locked = True api.app.state.pm.is_locked = True headers = {"Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw"} res = await cl.post("/api/v1/vault/unlock", headers=headers) assert res.status_code == 200 assert res.json()["status"] == "unlocked" assert "help_hint" in res.json() assert called["password"] == "pw" @pytest.mark.anyio async def test_vault_unlock_rate_limited_after_failed_attempts(client, monkeypatch): cl, token = client api.app.state.pm.verify_password = lambda _pw: False api.app.state.pm.unlock_vault = lambda _pw: 0.01 monkeypatch.setattr(api, "_UNLOCK_ATTEMPT_LIMIT", 2) monkeypatch.setattr(api, "_UNLOCK_ATTEMPT_WINDOW", 600) headers = {"Authorization": f"Bearer {token}", "X-SeedPass-Password": "bad"} res1 = await cl.post("/api/v1/vault/unlock", headers=headers) assert res1.status_code == 401 res2 = await cl.post("/api/v1/vault/unlock", headers=headers) assert res2.status_code == 401 res3 = await cl.post("/api/v1/vault/unlock", headers=headers) assert res3.status_code == 429 @pytest.mark.anyio async def test_entry_endpoints_blocked_when_vault_locked(client): cl, token = client api.app.state.pm.locked = True api.app.state.pm.is_locked = True headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/entry", params={"query": "x"}, headers=headers) assert res.status_code == 423 assert res.json()["detail"] == "Vault is locked" @pytest.mark.anyio async def test_generate_password_blocked_when_vault_locked(client): cl, token = client api.app.state.pm.locked = True api.app.state.pm.is_locked = True headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/password", headers=headers, json={"length": 12}) assert res.status_code == 423 assert res.json()["detail"] == "Vault is locked" @pytest.mark.anyio async def test_config_endpoint_blocked_when_vault_locked(client): cl, token = client api.app.state.pm.locked = True api.app.state.pm.is_locked = True headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/config/k", headers=headers) assert res.status_code == 423 assert res.json()["detail"] == "Vault is locked" @pytest.mark.anyio async def test_get_config_denies_sensitive_keys(client): cl, token = client api.app.state.pm.locked = False api.app.state.pm.is_locked = False headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/config/password_hash", headers=headers) assert res.status_code == 403 @pytest.mark.anyio async def test_lock_unlock_cycle_restores_entry_access(client): cl, token = client api.app.state.pm.locked = False api.app.state.pm.is_locked = False api.app.state.pm.verify_password = lambda pw: pw == "pw" api.app.state.pm.lock_vault = lambda: ( setattr(api.app.state.pm, "locked", True), setattr(api.app.state.pm, "is_locked", True), ) api.app.state.pm.unlock_vault = lambda pw: ( setattr(api.app.state.pm, "locked", False), setattr(api.app.state.pm, "is_locked", False), 0.01, )[-1] auth = {"Authorization": f"Bearer {token}"} locked_res = await cl.post("/api/v1/vault/lock", headers=auth) assert locked_res.status_code == 200 denied = await cl.get("/api/v1/entry", params={"query": "x"}, headers=auth) assert denied.status_code == 423 unlocked = await cl.post( "/api/v1/vault/unlock", headers={**auth, "X-SeedPass-Password": "pw"}, ) assert unlocked.status_code == 200 allowed = await cl.get("/api/v1/entry", params={"query": "x"}, headers=auth) assert allowed.status_code == 200 @pytest.mark.anyio async def test_secret_mode_endpoint(client): cl, token = client called = {} def set_secret(val): called.setdefault("enabled", val) def set_delay(val): called.setdefault("delay", val) api.app.state.pm.config_manager.set_secret_mode_enabled = set_secret api.app.state.pm.config_manager.set_clipboard_clear_delay = set_delay headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/secret-mode", json={"enabled": True, "delay": 12}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "ok"} assert called["enabled"] is True assert called["delay"] == 12 @pytest.mark.anyio async def test_vault_export_endpoint(client, tmp_path): cl, token = client out = tmp_path / "out.json" out.write_text("data") api.app.state.pm.handle_export_database = lambda *a, **k: out headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 200 assert res.content == b"data" res = await cl.post( "/api/v1/vault/export", headers={"Authorization": f"Bearer {token}"} ) assert res.status_code == 401 @pytest.mark.anyio async def test_vault_export_endpoint_denied_for_agent_profile( client, tmp_path, monkeypatch ): cl, token = client called = {} monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr( api, "record_export_policy_event", lambda event, details: called.setdefault("event", (event, details)), ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:full_export_blocked" assert called["event"][0] == "export_denied" @pytest.mark.anyio async def test_vault_export_endpoint_policy_filtered_manifest( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( '{"allow_kinds":["password"],"allow_export_import":false}', encoding="utf-8", ) api.app.state.pm.vault = SimpleNamespace( load_index=lambda: { "schema_version": 4, "entries": { "0": {"kind": "password", "label": "ok"}, "1": {"kind": "totp", "label": "skip"}, }, }, encryption_manager=SimpleNamespace(encrypt_data=lambda payload: payload), ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", "X-SeedPass-Policy-Filtered": "true", } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 200 payload = json.loads(res.content.decode("utf-8")) assert payload["_export_manifest"]["mode"] == "policy_filtered" assert payload["_export_manifest"]["allow_kinds"] == ["password"] assert payload["_export_manifest"]["included_entry_indexes"] == ["0"] @pytest.mark.anyio async def test_vault_export_endpoint_requires_approval_when_configured( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(approval_core, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( json.dumps( { "allow_kinds": ["password"], "allow_export_import": True, "export": {"allow_full_vault": True}, "approvals": {"require_for": ["export"]}, } ), encoding="utf-8", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:approval_required" @pytest.mark.anyio async def test_vault_export_endpoint_blocked_when_high_risk_locked( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(isolation_core, "APP_DIR", tmp_path) isolation_core.set_high_risk_factor("factor-abc") (tmp_path / "agent_policy.json").write_text( json.dumps( { "allow_export_import": True, "export": {"allow_full_vault": True}, "approvals": {"require_for": []}, "secret_isolation": {"enabled": True, "high_risk_kinds": ["seed"]}, } ), encoding="utf-8", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:high_risk_locked" @pytest.mark.anyio async def test_vault_export_endpoint_approval_allows_full_export( client, tmp_path, monkeypatch ): cl, token = client out = tmp_path / "out.json" out.write_text("data") api.app.state.pm.handle_export_database = lambda *a, **k: out monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(approval_core, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( json.dumps( { "allow_kinds": ["password"], "allow_export_import": True, "export": {"allow_full_vault": True}, "approvals": {"require_for": ["export"]}, } ), encoding="utf-8", ) approval = approval_core.issue_approval( action="export", ttl_seconds=300, uses=1, resource="vault:full", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", "X-SeedPass-Approval-Id": approval["id"], } res = await cl.post("/api/v1/vault/export", headers=headers) assert res.status_code == 200 assert res.content == b"data" @pytest.mark.anyio async def test_totp_export_endpoint_denied_for_agent_profile( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( '{"allow_kinds":["password"],"allow_export_import":false}', encoding="utf-8", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", } res = await cl.get("/api/v1/totp/export", headers=headers) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:kind_not_allowed" @pytest.mark.anyio async def test_export_check_endpoint_full_denied_default(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/export/check", params={"mode": "full"}, headers=headers) assert res.status_code == 200 payload = res.json() assert payload["status"] == "ok" assert payload["allowed"] is False assert payload["reason"] == "policy_deny:full_export_blocked" @pytest.mark.anyio async def test_export_check_endpoint_kind_allowed_default( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) headers = {"Authorization": f"Bearer {token}"} res = await cl.get( "/api/v1/export/check", params={"mode": "kind", "kind": "totp"}, headers=headers, ) assert res.status_code == 200 payload = res.json() assert payload["status"] == "ok" assert payload["allowed"] is True assert payload["reason"] == "policy_allow:kind_allowed" @pytest.mark.anyio async def test_export_check_endpoint_kind_requires_kind(client): cl, token = client headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/export/check", params={"mode": "kind"}, headers=headers) assert res.status_code == 400 assert res.json()["detail"] == "missing_kind" @pytest.mark.anyio async def test_export_manifest_verify_endpoint_ok(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) policy = export_policy.load_export_policy() package = export_policy.build_policy_filtered_export_package( {"schema_version": 4, "entries": {"0": {"kind": "password", "label": "ok"}}}, policy, ) headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/export/manifest/verify", headers=headers, json=package) assert res.status_code == 200 payload = res.json() assert payload["status"] == "ok" assert payload["valid"] is True assert payload["errors"] == [] @pytest.mark.anyio async def test_export_manifest_verify_endpoint_mismatch(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) package = export_policy.build_policy_filtered_export_package( {"schema_version": 4, "entries": {"0": {"kind": "password", "label": "ok"}}}, export_policy.load_export_policy(), ) # Change policy after package creation to force mismatch. (tmp_path / "agent_policy.json").write_text( '{"allow_kinds":["totp"],"allow_export_import":false}', encoding="utf-8", ) headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/export/manifest/verify", headers=headers, json=package) assert res.status_code == 200 payload = res.json() assert payload["status"] == "ok" assert payload["valid"] is False assert "policy_stamp_mismatch" in payload["errors"] @pytest.mark.anyio async def test_export_manifest_verify_endpoint_detects_tamper( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) policy = export_policy.load_export_policy() package = export_policy.build_policy_filtered_export_package( {"schema_version": 4, "entries": {"0": {"kind": "password", "label": "ok"}}}, policy, ) package["entries"]["0"]["kind"] = "ssh" headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/export/manifest/verify", headers=headers, json=package) assert res.status_code == 200 payload = res.json() assert payload["status"] == "ok" assert payload["valid"] is False assert "entry_kind_not_allowed" in payload["errors"] assert "entries_hash_mismatch" in payload["errors"] @pytest.mark.anyio async def test_backup_parent_seed_endpoint(client, tmp_path): cl, token = client api.app.state.pm.parent_seed = "seed" called = {} api.app.state.pm.encryption_manager = SimpleNamespace( encrypt_and_save_file=lambda data, path: called.setdefault("path", path), resolve_relative_path=lambda p: p, ) path = Path("seed.enc") headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": str(path), "confirm": True}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "saved", "path": str(path)} assert called["path"] == path res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": str(path)}, headers=headers, ) assert res.status_code == 400 @pytest.mark.anyio async def test_high_risk_api_unlock_status_and_lock(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(isolation_core, "APP_DIR", tmp_path) isolation_core.set_high_risk_factor("factor-123") api.app.state.pm.current_fingerprint = "ABC123" headers = {"Authorization": f"Bearer {token}"} status_before = await cl.get("/api/v1/high-risk/status", headers=headers) assert status_before.status_code == 200 before_payload = status_before.json() assert before_payload["status"] == "ok" assert before_payload["unlocked"] is False unlock_headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-High-Risk-Factor": "factor-123", } unlocked = await cl.post( "/api/v1/high-risk/unlock", json={"ttl": 120}, headers=unlock_headers, ) assert unlocked.status_code == 200 unlocked_payload = unlocked.json() assert unlocked_payload["status"] == "ok" assert unlocked_payload["fingerprint"] == "ABC123" status_after = await cl.get("/api/v1/high-risk/status", headers=headers) assert status_after.status_code == 200 assert status_after.json()["unlocked"] is True locked = await cl.post("/api/v1/high-risk/lock", headers=headers) assert locked.status_code == 200 assert locked.json()["status"] == "ok" status_final = await cl.get("/api/v1/high-risk/status", headers=headers) assert status_final.status_code == 200 assert status_final.json()["unlocked"] is False @pytest.mark.anyio async def test_agent_job_profiles_crud_endpoints(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(job_core, "APP_DIR", tmp_path) api.app.state.pm.current_fingerprint = "ABC123" headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/agent/job-profiles", headers=headers, json={"id": "nightly", "query": "Site", "auth_broker": "keyring"}, ) assert created.status_code == 200 created_payload = created.json() assert created_payload["status"] == "ok" assert created_payload["job_profile"]["id"] == "nightly" listed = await cl.get("/api/v1/agent/job-profiles", headers=headers) assert listed.status_code == 200 listed_payload = listed.json() assert any(v["id"] == "nightly" for v in listed_payload["job_profiles"]) revoked = await cl.delete("/api/v1/agent/job-profiles/nightly", headers=headers) assert revoked.status_code == 200 assert revoked.json()["status"] == "ok" @pytest.mark.anyio async def test_agent_job_profile_run_issues_lease(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(job_core, "APP_DIR", tmp_path) monkeypatch.setattr(lease_core, "APP_DIR", tmp_path) monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) api.app.state.pm.current_fingerprint = "ABC123" api.app.state.pm.entry_manager.search_entries = lambda q: [ (7, "Site", "user", "url", False, EntryType.PASSWORD) ] headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/agent/job-profiles", headers=headers, json={"id": "nightly-run", "query": "Site", "auth_broker": "keyring"}, ) assert created.status_code == 200 run = await cl.post( "/api/v1/agent/job-profiles/nightly-run/run", headers=headers, json={}, ) assert run.status_code == 200 payload = run.json() assert payload["status"] == "ok" assert payload["mode"] == "lease_issued" assert payload["kind"] == "password" assert payload["lease_id"] @pytest.mark.anyio async def test_agent_job_profile_run_policy_mismatch_denied( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(job_core, "APP_DIR", tmp_path) monkeypatch.setattr(lease_core, "APP_DIR", tmp_path) monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) api.app.state.pm.current_fingerprint = "ABC123" api.app.state.pm.entry_manager.search_entries = lambda q: [ (7, "Site", "user", "url", False, EntryType.PASSWORD) ] headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/agent/job-profiles", headers=headers, json={"id": "policy-run", "query": "Site", "auth_broker": "keyring"}, ) assert created.status_code == 200 (tmp_path / "agent_policy.json").write_text( json.dumps({"allow_kinds": ["totp"], "allow_export_import": False}), encoding="utf-8", ) denied = await cl.post( "/api/v1/agent/job-profiles/policy-run/run", headers=headers, json={}, ) assert denied.status_code == 403 assert denied.json()["detail"] == "job_profile_policy_mismatch" allowed = await cl.post( "/api/v1/agent/job-profiles/policy-run/run", headers=headers, json={"allow_policy_drift": True}, ) assert allowed.status_code == 200 assert allowed.json()["status"] == "ok" @pytest.mark.anyio async def test_agent_job_profile_template_endpoint(client, tmp_path, monkeypatch): cl, token = client monkeypatch.setattr(job_core, "APP_DIR", tmp_path) api.app.state.pm.current_fingerprint = "ABC123" headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/agent/job-profiles", headers=headers, json={ "id": "templated-job", "query": "Site", "auth_broker": "keyring", "schedule": "*/30 * * * *", }, ) assert created.status_code == 200 cron = await cl.get( "/api/v1/agent/job-profiles/templated-job/template", headers=headers, ) assert cron.status_code == 200 cron_payload = cron.json() assert cron_payload["status"] == "ok" assert cron_payload["mode"] == "cron" assert cron_payload["schedule_source"] == "profile" assert "agent job-profile-run templated-job" in cron_payload["command"] assert cron_payload["cron_line"].startswith("*/30 * * * * ") systemd = await cl.get( "/api/v1/agent/job-profiles/templated-job/template", headers=headers, params={ "mode": "systemd", "schedule": "*:0/20", "unit_name": "seedpass-nightly", }, ) assert systemd.status_code == 200 systemd_payload = systemd.json() assert systemd_payload["mode"] == "systemd" assert systemd_payload["schedule_source"] == "provided" assert ( "ExecStart=seedpass --fingerprint ABC123 agent job-profile-run templated-job" in systemd_payload["systemd_service"] ) assert "OnCalendar=*:0/20" in systemd_payload["systemd_timer"] assert "template_manifest" in systemd_payload assert systemd_payload["template_manifest"]["job_profile_id"] == "templated-job" @pytest.mark.anyio async def test_agent_job_profile_template_post_and_verify( client, tmp_path, monkeypatch ): cl, token = client monkeypatch.setattr(job_core, "APP_DIR", tmp_path) api.app.state.pm.current_fingerprint = "ABC123" headers = {"Authorization": f"Bearer {token}"} created = await cl.post( "/api/v1/agent/job-profiles", headers=headers, json={"id": "verify-job", "query": "Site", "auth_broker": "keyring"}, ) assert created.status_code == 200 rendered = await cl.post( "/api/v1/agent/job-profiles/verify-job/template", headers=headers, json={"mode": "cron", "schedule": "0 * * * *", "include_manifest": True}, ) assert rendered.status_code == 200 rendered_payload = rendered.json() assert rendered_payload["status"] == "ok" assert rendered_payload["schedule"] == "0 * * * *" assert "template_manifest" in rendered_payload verified = await cl.post( "/api/v1/agent/job-profiles/verify-job/template/verify", headers=headers, json={"template": rendered_payload}, ) assert verified.status_code == 200 verified_payload = verified.json() assert verified_payload["status"] == "ok" assert verified_payload["valid"] is True tampered = dict(rendered_payload) tampered["command"] = "seedpass --fingerprint ABC123 agent job-profile-run altered" bad = await cl.post( "/api/v1/agent/job-profiles/verify-job/template/verify", headers=headers, json={"template": tampered}, ) assert bad.status_code == 200 bad_payload = bad.json() assert bad_payload["valid"] is False assert "template_hash_sha256" in bad_payload["mismatches"] @pytest.mark.anyio async def test_agent_recovery_split_recover_api(client, monkeypatch, tmp_path): cl, token = client monkeypatch.setattr(recovery_core, "APP_DIR", tmp_path) headers = {"Authorization": f"Bearer {token}"} secret = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" split = await cl.post( "/api/v1/agent/recovery/split", headers=headers, json={"secret": secret, "shares": 5, "threshold": 3, "label": "api-recovery"}, ) assert split.status_code == 200 split_payload = split.json() assert split_payload["status"] == "ok" shares = split_payload["shares"] assert len(shares) == 5 recover = await cl.post( "/api/v1/agent/recovery/recover", headers=headers, json={"shares": shares[:3], "reveal": True}, ) assert recover.status_code == 200 recover_payload = recover.json() assert recover_payload["status"] == "ok" assert recover_payload["secret"] == secret @pytest.mark.anyio async def test_agent_recovery_drill_and_verify_api(client, monkeypatch, tmp_path): cl, token = client monkeypatch.setattr(recovery_core, "APP_DIR", tmp_path) headers = {"Authorization": f"Bearer {token}"} missing = tmp_path / "missing-backup.enc" drill = await cl.post( "/api/v1/agent/recovery/drill", headers=headers, json={"backup_path": str(missing), "simulated": True, "max_age_days": 7}, ) assert drill.status_code == 200 drill_payload = drill.json() assert drill_payload["status"] == "ok" assert drill_payload["report"]["status"] == "warning" listed = await cl.get("/api/v1/agent/recovery/drills", headers=headers) assert listed.status_code == 200 listed_payload = listed.json() assert listed_payload["count"] >= 1 verify = await cl.post( "/api/v1/agent/recovery/drills/verify", headers=headers, json={"limit": 50}, ) assert verify.status_code == 200 verify_payload = verify.json() assert verify_payload["status"] == "ok" assert verify_payload["valid"] is True @pytest.mark.anyio async def test_agent_recovery_drill_verify_detects_tamper( client, monkeypatch, tmp_path ): cl, token = client monkeypatch.setattr(recovery_core, "APP_DIR", tmp_path) headers = {"Authorization": f"Bearer {token}"} missing = tmp_path / "missing-backup.enc" created = await cl.post( "/api/v1/agent/recovery/drill", headers=headers, json={"backup_path": str(missing), "simulated": True}, ) assert created.status_code == 200 log_path = tmp_path / "agent_recovery_drills.log" lines = [ln for ln in log_path.read_text(encoding="utf-8").splitlines() if ln] payload = json.loads(lines[-1]) payload["backup_exists"] = True lines[-1] = json.dumps(payload, sort_keys=True) log_path.write_text("\n".join(lines) + "\n", encoding="utf-8") verify = await cl.post( "/api/v1/agent/recovery/drills/verify", headers=headers, json={"limit": 50}, ) assert verify.status_code == 200 verify_payload = verify.json() assert verify_payload["valid"] is False assert any("sig_mismatch_line" in v for v in verify_payload["errors"]) @pytest.mark.anyio async def test_backup_parent_seed_blocked_when_high_risk_locked( client, tmp_path, monkeypatch ): cl, token = client api.app.state.pm.parent_seed = "seed" api.app.state.pm.current_fingerprint = "ABC123" api.app.state.pm.encryption_manager = SimpleNamespace( encrypt_and_save_file=lambda data, path: None, resolve_relative_path=lambda p: p, ) monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(isolation_core, "APP_DIR", tmp_path) isolation_core.set_high_risk_factor("factor-xyz") (tmp_path / "agent_policy.json").write_text( json.dumps( {"secret_isolation": {"enabled": True, "high_risk_kinds": ["seed"]}} ), encoding="utf-8", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": "seed.enc", "confirm": True}, headers=headers, ) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:high_risk_locked" @pytest.mark.anyio async def test_backup_parent_seed_requires_approval_when_agent_profile( client, tmp_path, monkeypatch ): cl, token = client api.app.state.pm.parent_seed = "seed" api.app.state.pm.encryption_manager = SimpleNamespace( encrypt_and_save_file=lambda data, path: None, resolve_relative_path=lambda p: p, ) monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(approval_core, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( json.dumps({"approvals": {"require_for": ["reveal_parent_seed"]}}), encoding="utf-8", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", } res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": "seed.enc", "confirm": True}, headers=headers, ) assert res.status_code == 403 assert res.json()["detail"] == "policy_deny:approval_required" @pytest.mark.anyio async def test_backup_parent_seed_allows_with_approval_when_agent_profile( client, tmp_path, monkeypatch ): cl, token = client api.app.state.pm.parent_seed = "seed" called = {} api.app.state.pm.encryption_manager = SimpleNamespace( encrypt_and_save_file=lambda data, path: called.setdefault("path", path), resolve_relative_path=lambda p: p, ) monkeypatch.setattr(export_policy, "APP_DIR", tmp_path) monkeypatch.setattr(approval_core, "APP_DIR", tmp_path) (tmp_path / "agent_policy.json").write_text( json.dumps({"approvals": {"require_for": ["reveal_parent_seed"]}}), encoding="utf-8", ) approval = approval_core.issue_approval( action="reveal_parent_seed", ttl_seconds=300, uses=1, resource="vault:parent-seed", ) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", "X-SeedPass-Agent-Profile": "true", "X-SeedPass-Approval-Id": approval["id"], } res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": "seed.enc", "confirm": True}, headers=headers, ) assert res.status_code == 200 assert res.json() == {"status": "saved", "path": "seed.enc"} assert str(called["path"]) == "seed.enc" @pytest.mark.anyio async def test_backup_parent_seed_path_traversal_blocked(client, tmp_path): cl, token = client api.app.state.pm.parent_seed = "seed" key = base64.urlsafe_b64encode(os.urandom(32)) api.app.state.pm.encryption_manager = EncryptionManager(key, tmp_path) headers = { "Authorization": f"Bearer {token}", "X-SeedPass-Password": "pw", } res = await cl.post( "/api/v1/vault/backup-parent-seed", json={"path": "../evil.enc", "confirm": True}, headers=headers, ) assert res.status_code == 400 @pytest.mark.anyio async def test_relay_management_endpoints(client, dummy_nostr_client, monkeypatch): cl, token = client nostr_client, _ = dummy_nostr_client relays = ["wss://a", "wss://b"] def load_config(require_pin=False): return {"relays": relays.copy()} called = {} def set_relays(new, require_pin=False): called["set"] = new api.app.state.pm.config_manager.load_config = load_config api.app.state.pm.config_manager.set_relays = set_relays monkeypatch.setattr( NostrClient, "initialize_client_pool", lambda self: called.setdefault("init", True), ) monkeypatch.setattr( nostr_client, "close_client_pool", lambda: called.setdefault("close", True) ) api.app.state.pm.nostr_client = nostr_client api.app.state.pm.nostr_client.relays = relays.copy() headers = {"Authorization": f"Bearer {token}"} res = await cl.get("/api/v1/relays", headers=headers) assert res.status_code == 200 assert res.json() == {"relays": relays} res = await cl.post("/api/v1/relays", json={"url": "wss://c"}, headers=headers) assert res.status_code == 200 assert called["set"] == ["wss://a", "wss://b", "wss://c"] api.app.state.pm.config_manager.load_config = lambda require_pin=False: { "relays": ["wss://a", "wss://b", "wss://c"] } res = await cl.delete("/api/v1/relays/2", headers=headers) assert res.status_code == 200 assert called["set"] == ["wss://a", "wss://c"] res = await cl.post("/api/v1/relays/reset", headers=headers) assert res.status_code == 200 assert called.get("init") is True assert api.app.state.pm.nostr_client.relays == list(DEFAULT_RELAYS) @pytest.mark.anyio async def test_generate_password_no_special_chars(client): cl, token = client class DummyEnc: def derive_seed_from_mnemonic(self, mnemonic): return b"\x00" * 32 class DummyBIP85: def derive_entropy( self, index: int, entropy_bytes: int, app_no: int = 32 ) -> bytes: return bytes(range(entropy_bytes)) api.app.state.pm.password_generator = PasswordGenerator( DummyEnc(), "seed", DummyBIP85() ) api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}"} res = await cl.post( "/api/v1/password", json={"length": 16, "include_special_chars": False}, headers=headers, ) assert res.status_code == 200 pw = res.json()["password"] assert not any(c in string.punctuation for c in pw) @pytest.mark.anyio async def test_generate_password_allowed_chars(client): cl, token = client class DummyEnc: def derive_seed_from_mnemonic(self, mnemonic): return b"\x00" * 32 class DummyBIP85: def derive_entropy( self, index: int, entropy_bytes: int, app_no: int = 32 ) -> bytes: return bytes((index + i) % 256 for i in range(entropy_bytes)) api.app.state.pm.password_generator = PasswordGenerator( DummyEnc(), "seed", DummyBIP85() ) api.app.state.pm.parent_seed = "seed" headers = {"Authorization": f"Bearer {token}"} allowed = "@$" res = await cl.post( "/api/v1/password", json={"length": 16, "allowed_special_chars": allowed}, headers=headers, ) assert res.status_code == 200 pw = res.json()["password"] specials = [c for c in pw if c in string.punctuation] assert specials and all(c in allowed for c in specials) @pytest.mark.anyio async def test_semantic_endpoints(client, monkeypatch): cl, token = client class DummySemanticService: def __init__(self, _pm): self.enabled = True self.last_enabled = None self.last_search = None self.mode = "keyword" def status(self): return { "enabled": bool(self.enabled), "built": True, "records": 4, "mode": self.mode, } def build(self): return {"enabled": True, "built": True, "records": 4} def rebuild(self): return {"enabled": True, "built": True, "records": 4} def set_enabled(self, enabled: bool): self.last_enabled = bool(enabled) self.enabled = bool(enabled) return {"enabled": bool(enabled)} def set_mode(self, mode: str): self.mode = str(mode) return self.status() def search( self, query: str, *, k: int = 10, kind: str | None = None, mode: str | None = None, ): self.last_search = (query, k, kind, mode) return [{"entry_id": 10, "kind": kind or "document", "score": 0.5}] service = DummySemanticService(None) monkeypatch.setattr(api, "SemanticIndexService", lambda _pm: service) headers = {"Authorization": f"Bearer {token}"} status = await cl.get("/api/v1/semantic/status", headers=headers) assert status.status_code == 200 assert status.json()["records"] == 4 built = await cl.post("/api/v1/semantic/build", headers=headers) assert built.status_code == 200 assert built.json()["built"] is True rebuilt = await cl.post("/api/v1/semantic/rebuild", headers=headers) assert rebuilt.status_code == 200 assert rebuilt.json()["built"] is True configured = await cl.post( "/api/v1/semantic/config", json={"enabled": False, "mode": "hybrid"}, headers=headers, ) assert configured.status_code == 200 assert configured.json() == {"enabled": False, "mode": "hybrid"} assert service.last_enabled is False assert service.mode == "hybrid" searched = await cl.post( "/api/v1/semantic/search", json={"query": "relay notes", "k": 6, "kind": "document", "mode": "semantic"}, headers=headers, ) assert searched.status_code == 200 assert searched.json()["results"][0]["entry_id"] == 10 assert service.last_search == ("relay notes", 6, "document", "semantic") @pytest.mark.anyio async def test_semantic_search_requires_query(client): cl, token = client headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/semantic/search", json={}, headers=headers) assert res.status_code == 400 assert res.json() == {"detail": "Missing query"} @pytest.mark.anyio async def test_semantic_config_requires_enabled_or_mode(client): cl, token = client headers = {"Authorization": f"Bearer {token}"} res = await cl.post("/api/v1/semantic/config", json={}, headers=headers) assert res.status_code == 400 assert res.json() == {"detail": "Missing enabled or mode"}