-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathtest_admin_api.py
More file actions
128 lines (98 loc) · 4.86 KB
/
Copy pathtest_admin_api.py
File metadata and controls
128 lines (98 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""Tests for the admin surface: GET /admin/config + the admin_api feature flag.
These are deterministic (no LLM): the endpoint only reads server-level config. We
toggle env vars + clear the config cache to exercise the enable flag, the optional
admin token, and credential redaction.
"""
import httpx
import pytest
import pytest_asyncio
from hindsight_api.api import create_app
from hindsight_api.config import clear_config_cache
@pytest_asyncio.fixture
async def admin_client(memory):
"""Async test client for the FastAPI app (mock LLM)."""
app = create_app(memory, initialize_memory=False)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
yield client
def _set_env(monkeypatch, **values: str | None) -> None:
"""Set/unset env vars and reset the cached config so the next read reflects them."""
for key, value in values.items():
if value is None:
monkeypatch.delenv(key, raising=False)
else:
monkeypatch.setenv(key, value)
clear_config_cache()
@pytest.fixture(autouse=True)
def _restore_config_cache():
"""Ensure the global config cache is reset after each test."""
yield
clear_config_cache()
@pytest.mark.asyncio
async def test_admin_config_disabled_by_default(admin_client, monkeypatch):
"""When the admin API is disabled (default), the endpoint is invisible (404)."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API=None, HINDSIGHT_API_ADMIN_TOKEN=None)
response = await admin_client.get("/admin/config")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_admin_config_enabled_no_token(admin_client, monkeypatch):
"""When enabled without a token, the endpoint is open and returns config."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="true", HINDSIGHT_API_ADMIN_TOKEN=None)
response = await admin_client.get("/admin/config")
assert response.status_code == 200
config = response.json()["config"]
# A representative spread of non-credential fields should be present.
assert "llm_provider" in config
assert "enable_admin_api" in config
assert config["enable_admin_api"] is True
@pytest.mark.asyncio
async def test_admin_config_redacts_credentials(admin_client, monkeypatch):
"""Credential fields are masked, never returned in cleartext."""
_set_env(
monkeypatch,
HINDSIGHT_API_ENABLE_ADMIN_API="true",
HINDSIGHT_API_ADMIN_TOKEN="s3cret-token",
HINDSIGHT_API_LLM_API_KEY="super-secret-key",
)
response = await admin_client.get("/admin/config", headers={"Authorization": "Bearer s3cret-token"})
assert response.status_code == 200
config = response.json()["config"]
# The configured LLM key is present but masked.
assert config["llm_api_key"] == "***"
assert "super-secret-key" not in response.text
# Provider keys that fall back to the LLM key (and aren't in the credential
# denylist) must also be masked — the view redacts by name, not just the set.
assert config["embeddings_openrouter_api_key"] == "***"
assert config["reranker_openrouter_api_key"] == "***"
# The admin token must never leak through its own config view.
assert config["admin_api_token"] == "***"
assert "s3cret-token" not in response.text
# Value-bearing fields that merely contain "token" in their name (plural) are
# NOT redacted — they carry useful config, not secrets.
assert config["recall_max_tokens"] != "***"
@pytest.mark.asyncio
async def test_admin_config_requires_token_when_set(admin_client, monkeypatch):
"""With a token configured, missing/wrong tokens are rejected; the right one passes."""
_set_env(
monkeypatch,
HINDSIGHT_API_ENABLE_ADMIN_API="true",
HINDSIGHT_API_ADMIN_TOKEN="right-token",
)
missing = await admin_client.get("/admin/config")
assert missing.status_code == 401
wrong = await admin_client.get("/admin/config", headers={"Authorization": "Bearer wrong-token"})
assert wrong.status_code == 401
bearer = await admin_client.get("/admin/config", headers={"Authorization": "Bearer right-token"})
assert bearer.status_code == 200
# A bare token (no "Bearer " prefix) is also accepted.
bare = await admin_client.get("/admin/config", headers={"Authorization": "right-token"})
assert bare.status_code == 200
@pytest.mark.asyncio
async def test_version_reports_admin_api_flag(admin_client, monkeypatch):
"""The /version feature flags track the admin enable flag."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="true")
enabled = await admin_client.get("/version")
assert enabled.json()["features"]["admin_api"] is True
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="false")
disabled = await admin_client.get("/version")
assert disabled.json()["features"]["admin_api"] is False