Skip to content

Commit c0a8606

Browse files
SimplicityGuyclaude
andcommitted
tests: add targeted tests to cover missing lines in explore, snapshot, user routers and config
- explore.py:46-47 (_get_optional_user ValueError branch) — direct async test with invalid JWT - snapshot.py:30 (503 when _jwt_secret is None) and :33-34 (401 on bad token) - user.py:42-43 (_get_optional_user ValueError branch on /status with bad token) - config.py:390,392 (ApiConfig missing POSTGRES_PASSWORD/DATABASE individual branches) - config.py:480,482,486,490,492 (CuratorConfig individual missing-var branches) - config.py:13 mark TYPE_CHECKING import with pragma: no cover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f08e173 commit c0a8606

5 files changed

Lines changed: 164 additions & 1 deletion

File tree

common/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
if TYPE_CHECKING:
13-
from collections.abc import Sequence
13+
from collections.abc import Sequence # pragma: no cover
1414

1515
import orjson
1616
import structlog

tests/api/test_explore.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,25 @@ def test_expand_valid_type_invalid_category(self, test_client: TestClient) -> No
393393
def test_expand_genre_invalid_category(self, test_client: TestClient) -> None:
394394
response = test_client.get("/api/expand?node_id=Rock&type=genre&category=nonexistent")
395395
assert response.status_code == 400
396+
397+
398+
class TestGetOptionalUserInvalidToken:
399+
"""Tests for _get_optional_user with an invalid token (explore router)."""
400+
401+
@pytest.mark.asyncio
402+
async def test_invalid_token_returns_none(self) -> None:
403+
"""explore.py:46-47 — bad Bearer token causes ValueError which returns None."""
404+
from unittest.mock import MagicMock
405+
406+
import api.routers.explore as explore_module
407+
from api.routers.explore import _get_optional_user
408+
409+
original = explore_module._jwt_secret
410+
explore_module._jwt_secret = "test-jwt-secret-for-unit-tests"
411+
try:
412+
creds = MagicMock()
413+
creds.credentials = "not.a.valid.jwt"
414+
result = await _get_optional_user(creds)
415+
assert result is None
416+
finally:
417+
explore_module._jwt_secret = original

tests/api/test_snapshot.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,30 @@ def test_restore_snapshot_expired(self, test_client: TestClient) -> None:
9292
assert response.status_code == 404
9393
finally:
9494
store._store.pop(token, None)
95+
96+
97+
class TestSnapshotAuth:
98+
"""Tests for _get_current_user in snapshot router."""
99+
100+
def test_no_jwt_secret_returns_503(self, test_client: TestClient, auth_headers: dict[str, str]) -> None:
101+
"""snapshot.py:30 — 503 when _jwt_secret is None."""
102+
import api.routers.snapshot as snap_module
103+
104+
original = snap_module._jwt_secret
105+
snap_module._jwt_secret = None
106+
try:
107+
body = {"nodes": [{"id": "1", "type": "artist"}], "center": {"id": "1", "type": "artist"}}
108+
response = test_client.post("/api/snapshot", json=body, headers=auth_headers)
109+
assert response.status_code == 503
110+
finally:
111+
snap_module._jwt_secret = original
112+
113+
def test_invalid_token_returns_401(self, test_client: TestClient) -> None:
114+
"""snapshot.py:33-34 — 401 on bad token."""
115+
body = {"nodes": [{"id": "1", "type": "artist"}], "center": {"id": "1", "type": "artist"}}
116+
response = test_client.post(
117+
"/api/snapshot",
118+
json=body,
119+
headers={"Authorization": "Bearer not.a.valid.jwt"},
120+
)
121+
assert response.status_code == 401

tests/api/test_user.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,19 @@ def test_error_message_format(self, test_client: TestClient, auth_headers: dict[
300300
ids = ",".join(str(i) for i in range(101))
301301
data = test_client.get(f"/api/user/status?ids={ids}", headers=auth_headers).json()
302302
assert data["error"] == "Too many IDs: maximum is 100"
303+
304+
305+
class TestGetOptionalUserInvalidToken:
306+
"""Tests for _get_optional_user with an invalid token (user router)."""
307+
308+
def test_invalid_token_returns_false_flags(self, test_client: TestClient) -> None:
309+
"""user.py:42-43 — bad token on optional-auth /status falls back to all-False."""
310+
response = test_client.get(
311+
"/api/user/status?ids=1,2",
312+
headers={"Authorization": "Bearer not.a.valid.jwt"},
313+
)
314+
assert response.status_code == 200
315+
data = response.json()
316+
for _rid, flags in data["status"].items():
317+
assert flags["in_collection"] is False
318+
assert flags["in_wantlist"] is False

tests/test_config.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,3 +730,101 @@ def test_curator_config_works_without_jwt_secret(self, monkeypatch: pytest.Monke
730730
# Should not raise
731731
config = CuratorConfig.from_env()
732732
assert not hasattr(config, "jwt_secret_key") or config.jwt_secret_key is None # type: ignore[attr-defined]
733+
734+
735+
class TestConfigMissingVars:
736+
"""Tests for individual missing-variable branches in ApiConfig and CuratorConfig."""
737+
738+
def test_api_config_missing_postgres_password(self, monkeypatch: pytest.MonkeyPatch) -> None:
739+
"""config.py:390 — POSTGRES_PASSWORD missing fires the append."""
740+
from common.config import ApiConfig
741+
742+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
743+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
744+
monkeypatch.delenv("POSTGRES_PASSWORD", raising=False)
745+
monkeypatch.setenv("POSTGRES_DATABASE", "db")
746+
monkeypatch.setenv("JWT_SECRET_KEY", "secret")
747+
with pytest.raises(ValueError, match="POSTGRES_PASSWORD"):
748+
ApiConfig.from_env()
749+
750+
def test_api_config_missing_postgres_database(self, monkeypatch: pytest.MonkeyPatch) -> None:
751+
"""config.py:392 — POSTGRES_DATABASE missing fires the append."""
752+
from common.config import ApiConfig
753+
754+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
755+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
756+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
757+
monkeypatch.delenv("POSTGRES_DATABASE", raising=False)
758+
monkeypatch.setenv("JWT_SECRET_KEY", "secret")
759+
with pytest.raises(ValueError, match="POSTGRES_DATABASE"):
760+
ApiConfig.from_env()
761+
762+
def test_curator_config_missing_postgres_address(self, monkeypatch: pytest.MonkeyPatch) -> None:
763+
"""config.py:480 — POSTGRES_ADDRESS missing."""
764+
from common.config import CuratorConfig
765+
766+
monkeypatch.delenv("POSTGRES_ADDRESS", raising=False)
767+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
768+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
769+
monkeypatch.setenv("POSTGRES_DATABASE", "db")
770+
monkeypatch.setenv("NEO4J_ADDRESS", "bolt://localhost")
771+
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
772+
monkeypatch.setenv("NEO4J_PASSWORD", "neo4jpass")
773+
with pytest.raises(ValueError, match="POSTGRES_ADDRESS"):
774+
CuratorConfig.from_env()
775+
776+
def test_curator_config_missing_postgres_username(self, monkeypatch: pytest.MonkeyPatch) -> None:
777+
"""config.py:482 — POSTGRES_USERNAME missing."""
778+
from common.config import CuratorConfig
779+
780+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
781+
monkeypatch.delenv("POSTGRES_USERNAME", raising=False)
782+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
783+
monkeypatch.setenv("POSTGRES_DATABASE", "db")
784+
monkeypatch.setenv("NEO4J_ADDRESS", "bolt://localhost")
785+
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
786+
monkeypatch.setenv("NEO4J_PASSWORD", "neo4jpass")
787+
with pytest.raises(ValueError, match="POSTGRES_USERNAME"):
788+
CuratorConfig.from_env()
789+
790+
def test_curator_config_missing_postgres_database(self, monkeypatch: pytest.MonkeyPatch) -> None:
791+
"""config.py:486 — POSTGRES_DATABASE missing."""
792+
from common.config import CuratorConfig
793+
794+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
795+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
796+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
797+
monkeypatch.delenv("POSTGRES_DATABASE", raising=False)
798+
monkeypatch.setenv("NEO4J_ADDRESS", "bolt://localhost")
799+
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
800+
monkeypatch.setenv("NEO4J_PASSWORD", "neo4jpass")
801+
with pytest.raises(ValueError, match="POSTGRES_DATABASE"):
802+
CuratorConfig.from_env()
803+
804+
def test_curator_config_missing_neo4j_username(self, monkeypatch: pytest.MonkeyPatch) -> None:
805+
"""config.py:490 — NEO4J_USERNAME missing."""
806+
from common.config import CuratorConfig
807+
808+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
809+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
810+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
811+
monkeypatch.setenv("POSTGRES_DATABASE", "db")
812+
monkeypatch.setenv("NEO4J_ADDRESS", "bolt://localhost")
813+
monkeypatch.delenv("NEO4J_USERNAME", raising=False)
814+
monkeypatch.setenv("NEO4J_PASSWORD", "neo4jpass")
815+
with pytest.raises(ValueError, match="NEO4J_USERNAME"):
816+
CuratorConfig.from_env()
817+
818+
def test_curator_config_missing_neo4j_password(self, monkeypatch: pytest.MonkeyPatch) -> None:
819+
"""config.py:492 — NEO4J_PASSWORD missing."""
820+
from common.config import CuratorConfig
821+
822+
monkeypatch.setenv("POSTGRES_ADDRESS", "localhost")
823+
monkeypatch.setenv("POSTGRES_USERNAME", "user")
824+
monkeypatch.setenv("POSTGRES_PASSWORD", "pass")
825+
monkeypatch.setenv("POSTGRES_DATABASE", "db")
826+
monkeypatch.setenv("NEO4J_ADDRESS", "bolt://localhost")
827+
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
828+
monkeypatch.delenv("NEO4J_PASSWORD", raising=False)
829+
with pytest.raises(ValueError, match="NEO4J_PASSWORD"):
830+
CuratorConfig.from_env()

0 commit comments

Comments
 (0)