gabriel / musehub public
test_mist_phase4_router_registration.py python
130 lines 5.5 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
1 """Phase 4 TDD: Mist JSON API router explicit registration in main.py.
2
3 Tests are written RED first. Run before touching main.py and
4 musehub/api/routes/musehub/__init__.py to confirm failure, then implement.
5
6 The mists router must be:
7 1. Listed in _DIRECT_REGISTERED in __init__.py (excluded from auto-discovery).
8 2. Explicitly imported and included in main.py alongside api_identities_router,
9 api_orgs_router, etc.
10 3. Reachable via the OpenAPI schema at /api/mists paths.
11
12 These tests do not require DB state — they operate against the live app
13 instance and its route table.
14 """
15 from __future__ import annotations
16
17 import pytest
18 from httpx import AsyncClient
19
20
21 class TestMistsRouterDirectRegistration:
22 def test_mists_excluded_from_auto_discovery(self) -> None:
23 """'mists' must appear in _DIRECT_REGISTERED so the package __init__
24 does not auto-include it."""
25 from musehub.api.routes.musehub import _DIRECT_REGISTERED
26 assert "mists" in _DIRECT_REGISTERED, (
27 "'mists' must be in _DIRECT_REGISTERED to prevent double-registration"
28 )
29
30 def test_mists_router_imported_in_main(self) -> None:
31 """main.py must import the mists router explicitly."""
32 import musehub.main as _main
33 src = open(_main.__file__).read()
34 assert "mists" in src, (
35 "main.py must explicitly import or reference the mists router"
36 )
37
38 def test_mists_router_registered_via_include_router(self) -> None:
39 """main.py must call app.include_router with the mists router."""
40 import musehub.main as _main
41 src = open(_main.__file__).read()
42 # The module import line + the include_router call must both be present.
43 assert "api_mists_router" in src or "mists_router" in src or (
44 "from musehub.api.routes.musehub.mists import router" in src
45 ), "main.py must explicitly include the mists router by name"
46
47
48 class TestMistsRouterOpenAPI:
49 @pytest.mark.asyncio
50 async def test_mists_paths_in_openapi_schema(self, client: AsyncClient) -> None:
51 """GET /api/openapi.json must list /api/mists paths."""
52 r = await client.get("/api/openapi.json")
53 assert r.status_code == 200
54 schema = r.json()
55 paths = schema.get("paths", {})
56 mist_paths = [p for p in paths if "/mists" in p]
57 assert len(mist_paths) > 0, (
58 f"No /mists paths found in OpenAPI schema. "
59 f"Registered paths sample: {list(paths.keys())[:20]}"
60 )
61
62 @pytest.mark.asyncio
63 async def test_create_mist_operation_in_schema(self, client: AsyncClient) -> None:
64 r = await client.get("/api/openapi.json")
65 schema = r.json()
66 paths = schema.get("paths", {})
67 assert "/api/mists" in paths or any(
68 p.endswith("/mists") for p in paths
69 ), f"POST /api/mists not found in schema paths: {[p for p in paths if 'mist' in p]}"
70
71 @pytest.mark.asyncio
72 async def test_explore_mists_operation_in_schema(self, client: AsyncClient) -> None:
73 r = await client.get("/api/openapi.json")
74 schema = r.json()
75 paths = schema.get("paths", {})
76 assert any("mists/explore" in p for p in paths), (
77 f"GET /api/mists/explore not found in schema"
78 )
79
80
81 class TestMistsRouterEndpoints:
82 @pytest.mark.asyncio
83 async def test_explore_endpoint_returns_200(self, client: AsyncClient) -> None:
84 """GET /api/mists/explore must return 200, not 404."""
85 r = await client.get("/api/mists/explore")
86 assert r.status_code == 200, (
87 f"Expected 200 from /api/mists/explore; got {r.status_code} — "
88 "router may not be registered"
89 )
90
91 @pytest.mark.asyncio
92 async def test_explore_response_is_valid_json(self, client: AsyncClient) -> None:
93 r = await client.get("/api/mists/explore")
94 assert r.status_code == 200
95 body = r.json()
96 assert "mists" in body
97
98 @pytest.mark.asyncio
99 async def test_get_nonexistent_mist_returns_404_not_422(
100 self, client: AsyncClient
101 ) -> None:
102 """A valid mist_id that doesn't exist should return 404, not 422 (unprocessable
103 entity from FastAPI if the route is missing or double-registered)."""
104 r = await client.get("/api/mists/Abc123Xyz789")
105 assert r.status_code in (404, 200), (
106 f"Expected 404 for unknown mist, got {r.status_code} — "
107 "422 indicates a routing problem (possibly double-registration)"
108 )
109
110 @pytest.mark.asyncio
111 async def test_no_duplicate_operation_ids(self, client: AsyncClient) -> None:
112 """Double-registration causes duplicate operationIds — FastAPI raises on startup."""
113 r = await client.get("/api/openapi.json")
114 assert r.status_code == 200, (
115 "OpenAPI schema returned non-200 — likely duplicate operationId "
116 "from double-registering the mists router"
117 )
118 schema = r.json()
119 # Collect all operationIds from mist-related paths.
120 op_ids: list[str] = []
121 for path, methods in schema.get("paths", {}).items():
122 if "mist" not in path:
123 continue
124 for method_data in methods.values():
125 if isinstance(method_data, dict) and "operationId" in method_data:
126 op_ids.append(method_data["operationId"])
127 assert len(op_ids) == len(set(op_ids)), (
128 f"Duplicate operationIds detected — router registered twice: "
129 f"{[x for x in op_ids if op_ids.count(x) > 1]}"
130 )
File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 33 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 35 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago