gabriel / musehub public
test_musehub_negotiate.py python
172 lines 6.2 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Unit tests for the MuseHub content negotiation helper.
2
3 Covers — negotiate_response() dispatches HTML vs JSON based on
4 Accept header and ?format query param.
5
6 Tests:
7 - test_negotiate_wants_json_format_param — ?format=json → JSON path
8 - test_negotiate_wants_json_accept_header — Accept: application/json → JSON path
9 - test_negotiate_wants_html_by_default — no header/param → HTML path
10 - test_negotiate_wants_html_text_html_header — Accept: text/html → HTML path
11 - test_negotiate_json_uses_pydantic_by_alias — camelCase keys in JSON output
12 - test_negotiate_json_fallback_to_context — no json_data → context dict as JSON
13 - test_negotiate_accept_partial_match — mixed Accept header containing json
14 """
15 from __future__ import annotations
16
17 from unittest.mock import AsyncMock, MagicMock
18
19 import pytest
20 from fastapi.responses import JSONResponse
21 from starlette.responses import Response
22
23 from musehub.api.routes.musehub.negotiate import _wants_json, negotiate_response
24 from musehub.models.base import CamelModel
25
26
27 # ---------------------------------------------------------------------------
28 # _wants_json unit tests (synchronous helper — no I/O)
29 # ---------------------------------------------------------------------------
30
31
32 def _make_request(accept: str = "", format_param: str | None = None) -> MagicMock:
33 """Build a minimal mock Request with the given Accept header."""
34 req = MagicMock()
35 req.headers = {"accept": accept} if accept else {}
36 return req
37
38
39 def test_negotiate_wants_json_format_param() -> None:
40 """?format=json forces JSON regardless of Accept header."""
41 req = _make_request(accept="text/html")
42 assert _wants_json(req, format_param="json") is True
43
44
45 def test_negotiate_wants_json_accept_header() -> None:
46 """Accept: application/json triggers JSON path."""
47 req = _make_request(accept="application/json")
48 assert _wants_json(req, format_param=None) is True
49
50
51 def test_negotiate_wants_html_by_default() -> None:
52 """No Accept header and no format param → HTML (default)."""
53 req = _make_request()
54 assert _wants_json(req, format_param=None) is False
55
56
57 def test_negotiate_wants_html_text_html_header() -> None:
58 """Explicit Accept: text/html → HTML path."""
59 req = _make_request(accept="text/html,application/xhtml+xml")
60 assert _wants_json(req, format_param=None) is False
61
62
63 def test_negotiate_accept_partial_match() -> None:
64 """Mixed Accept containing application/json → JSON path."""
65 req = _make_request(accept="text/html, application/json;q=0.9")
66 assert _wants_json(req, format_param=None) is True
67
68
69 def test_negotiate_format_param_not_json_means_html() -> None:
70 """?format=html (or any non-json value) → HTML path."""
71 req = _make_request(accept="")
72 assert _wants_json(req, format_param="html") is False
73
74
75 # ---------------------------------------------------------------------------
76 # negotiate_response async tests (full response construction)
77 # ---------------------------------------------------------------------------
78
79
80 class _SampleModel(CamelModel):
81 """Minimal CamelModel for testing camelCase serialisation via by_alias=True."""
82
83 repo_id: str
84 star_count: int
85
86
87 @pytest.mark.anyio
88 async def test_negotiate_json_uses_pydantic_by_alias() -> None:
89 """JSON path serialises Pydantic model with camelCase keys (by_alias=True)."""
90 req = _make_request(accept="application/json")
91 templates = MagicMock()
92
93 model = _SampleModel(repo_id="abc-123", star_count=42)
94 resp = await negotiate_response(
95 request=req,
96 template_name="musehub/pages/repo.html",
97 context={"repo_id": "abc-123"},
98 templates=templates,
99 json_data=model,
100 format_param=None,
101 )
102 assert isinstance(resp, JSONResponse)
103 import json
104 body_bytes = bytes(resp.body) if isinstance(resp.body, memoryview) else resp.body
105 payload = json.loads(body_bytes)
106 assert "repoId" in payload, f"Expected camelCase 'repoId', got keys: {list(payload)}"
107 assert "starCount" in payload, f"Expected camelCase 'starCount', got keys: {list(payload)}"
108 assert payload["repoId"] == "abc-123"
109 assert payload["starCount"] == 42
110 templates.TemplateResponse.assert_not_called()
111
112
113 @pytest.mark.anyio
114 async def test_negotiate_json_fallback_to_context() -> None:
115 """When json_data is None, JSON path returns serialisable context values."""
116 req = _make_request(accept="application/json")
117 templates = MagicMock()
118
119 resp = await negotiate_response(
120 request=req,
121 template_name="musehub/pages/repo.html",
122 context={"owner": "alice", "repo_slug": "my-beats", "count": 3},
123 templates=templates,
124 json_data=None,
125 format_param=None,
126 )
127 assert isinstance(resp, JSONResponse)
128 import json
129 body_bytes = bytes(resp.body) if isinstance(resp.body, memoryview) else resp.body
130 payload = json.loads(body_bytes)
131 assert payload["owner"] == "alice"
132 assert payload["repo_slug"] == "my-beats"
133 assert payload["count"] == 3
134
135
136 @pytest.mark.anyio
137 async def test_negotiate_html_path_calls_template_response() -> None:
138 """HTML path delegates to templates.TemplateResponse."""
139 req = _make_request(accept="text/html")
140 mock_template_resp = MagicMock()
141 templates = MagicMock()
142 templates.TemplateResponse.return_value = mock_template_resp
143
144 resp = await negotiate_response(
145 request=req,
146 template_name="musehub/pages/repo.html",
147 context={"owner": "alice"},
148 templates=templates,
149 json_data=None,
150 format_param=None,
151 )
152 templates.TemplateResponse.assert_called_once_with(req, "musehub/pages/repo.html", {"owner": "alice"})
153 assert resp is mock_template_resp
154
155
156 @pytest.mark.anyio
157 async def test_negotiate_format_param_overrides_html_accept() -> None:
158 """?format=json forces JSON even when Accept: text/html."""
159 req = _make_request(accept="text/html")
160 templates = MagicMock()
161
162 model = _SampleModel(repo_id="xyz", star_count=0)
163 resp = await negotiate_response(
164 request=req,
165 template_name="musehub/pages/repo.html",
166 context={},
167 templates=templates,
168 json_data=model,
169 format_param="json",
170 )
171 assert isinstance(resp, JSONResponse)
172 templates.TemplateResponse.assert_not_called()
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago