"""Unit tests for routes/vault_status.py — minimal one-test-per-feature (P3.B). These tests confirm the handler shape + payload contract independently of a running hermes-webui. Integration tests against a real webui live in tests/integration/. """ import json from unittest.mock import MagicMock, patch from routes import vault_status class _FakeHandler: """Minimal stand-in for the http.server handler the route receives.""" def __init__(self): self.status = None self.headers = {} self.body = b"" def send_response(self, code): self.status = code def send_header(self, k, v): self.headers[k] = v def end_headers(self): pass @property def wfile(self): outer = self class _W: def write(self_inner, b): outer.body += b return _W() def test_register_wires_one_route(): """register() calls api.register_route exactly once for /api/vault/status.""" api = MagicMock() vault_status.register(api) api.register_route.assert_called_once() args = api.register_route.call_args[0] assert args[0] == "/api/vault/status" assert args[1] == "GET" def test_handler_returns_secrets_array_on_credctl_success(): """credctl list output → JSON {'secrets': [{'name': X}, ...]}.""" sample = "gitea\nmailchimp\nwoocommerce\n" with patch("routes.vault_status.subprocess.run") as run: run.return_value = MagicMock(stdout=sample, returncode=0) h = _FakeHandler() vault_status._handle_vault_status(h, None) assert h.status == 200 payload = json.loads(h.body.decode()) names = {s["name"] for s in payload["secrets"]} assert names == {"gitea", "mailchimp", "woocommerce"} def test_handler_returns_empty_list_on_credctl_failure(): """credctl missing or erroring → empty list, never raises.""" with patch("routes.vault_status.subprocess.run", side_effect=FileNotFoundError): h = _FakeHandler() vault_status._handle_vault_status(h, None) assert h.status == 200 payload = json.loads(h.body.decode()) assert payload == {"secrets": []}