gabriel / muse public
test_transport_hub_json.py python
254 lines 13.5 KB
Raw
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor ⚠ breaking 20 days ago
1 """TDD tests for HttpTransport.hub_json and HttpTransport.hub_bytes."""
2
3 from __future__ import annotations
4
5 import json
6 import unittest.mock
7
8 import pytest
9
10 from muse.core.transport import HttpTransport, SigningIdentity, TransportError
11
12
13 # ---------------------------------------------------------------------------
14 # Helpers (mirror test_core_transport.py patterns)
15 # ---------------------------------------------------------------------------
16
17
18 def _make_signing() -> SigningIdentity:
19 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
20 return SigningIdentity(handle="testuser", private_key=Ed25519PrivateKey.generate())
21
22
23 def _mock_httpx_client_resp(body: bytes, status: int = 200) -> unittest.mock.MagicMock:
24 resp = unittest.mock.MagicMock()
25 resp.status_code = status
26 resp.content = body
27 resp.text = body.decode("utf-8", errors="replace")
28 client = unittest.mock.MagicMock()
29 client.is_closed = False
30 client.request = unittest.mock.MagicMock(return_value=resp)
31 client.__enter__ = unittest.mock.MagicMock(return_value=client)
32 client.__exit__ = unittest.mock.MagicMock(return_value=False)
33 return client
34
35
36 # ---------------------------------------------------------------------------
37 # hub_json
38 # ---------------------------------------------------------------------------
39
40
41 class TestHubJson:
42 def test_returns_parsed_dict(self) -> None:
43 payload = {"muse_version": "0.1.0", "mist_id": "abc123", "ok": True}
44 client = _mock_httpx_client_resp(json.dumps(payload).encode())
45 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
46 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
47 result = HttpTransport().hub_json("GET", "https://hub.example.com/mist/abc123", None)
48 assert result == payload
49
50 def test_post_with_body_sends_json_bytes(self) -> None:
51 client = _mock_httpx_client_resp(b'{"ok": true}')
52 body = {"title": "hello", "content": "world"}
53 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
54 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
55 HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body=body)
56 call_kwargs = client.request.call_args
57 sent_body = call_kwargs.kwargs.get("content") or call_kwargs[1].get("content")
58 assert sent_body == json.dumps(body).encode()
59
60 def test_post_with_body_sets_content_type_json(self) -> None:
61 client = _mock_httpx_client_resp(b'{"ok": true}')
62 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
63 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
64 HttpTransport().hub_json(
65 "POST", "https://hub.example.com/mists", None, body={"x": 1}
66 )
67 headers = client.request.call_args.kwargs.get("headers", {})
68 ct = headers.get("Content-Type") or headers.get("content-type", "")
69 assert "application/json" in ct
70
71 def test_accept_json_header_sent(self) -> None:
72 client = _mock_httpx_client_resp(b'{"ok": true}')
73 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
74 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
75 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
76 headers = client.request.call_args.kwargs.get("headers", {})
77 accept = headers.get("Accept") or headers.get("accept", "")
78 assert "application/json" in accept
79
80 def test_msign_auth_header_sent_when_signing_provided(self) -> None:
81 signing = _make_signing()
82 client = _mock_httpx_client_resp(b'{"ok": true}')
83 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
84 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
85 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", signing)
86 headers = client.request.call_args.kwargs.get("headers", {})
87 auth = headers.get("Authorization") or headers.get("authorization", "")
88 assert auth.startswith('MSign handle="testuser"')
89
90 def test_no_auth_header_when_signing_is_none(self) -> None:
91 client = _mock_httpx_client_resp(b'{"ok": true}')
92 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
93 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
94 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
95 headers = client.request.call_args.kwargs.get("headers", {})
96 auth = headers.get("Authorization") or headers.get("authorization")
97 assert auth is None
98
99 def test_non_dict_json_response_returns_empty_dict(self) -> None:
100 client = _mock_httpx_client_resp(b'["a", "b", "c"]')
101 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
102 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
103 result = HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
104 assert result == {}
105
106 def test_invalid_json_raises_transport_error(self) -> None:
107 client = _mock_httpx_client_resp(b"not json at all")
108 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
109 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
110 with pytest.raises(TransportError) as exc_info:
111 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
112 assert "invalid json" in str(exc_info.value).lower()
113
114 def test_http_404_raises_transport_error(self) -> None:
115 client = _mock_httpx_client_resp(b"Not Found", status=404)
116 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
117 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
118 with pytest.raises(TransportError) as exc_info:
119 HttpTransport().hub_json("GET", "https://hub.example.com/mist/missing", None)
120 assert exc_info.value.status_code == 404
121
122 def test_http_401_raises_transport_error(self) -> None:
123 client = _mock_httpx_client_resp(b"Unauthorized", status=401)
124 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
125 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
126 with pytest.raises(TransportError) as exc_info:
127 HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body={})
128 assert exc_info.value.status_code == 401
129
130 def test_http_500_raises_transport_error(self) -> None:
131 client = _mock_httpx_client_resp(b"Server Error", status=500)
132 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
133 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
134 with pytest.raises(TransportError) as exc_info:
135 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
136 assert exc_info.value.status_code == 500
137
138 def test_network_error_raises_transport_error_with_code_0(self) -> None:
139 client = unittest.mock.MagicMock()
140 client.is_closed = False
141 client.request = unittest.mock.MagicMock(side_effect=Exception("connection refused"))
142 client.__enter__ = unittest.mock.MagicMock(return_value=client)
143 client.__exit__ = unittest.mock.MagicMock(return_value=False)
144 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
145 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
146 with pytest.raises(TransportError) as exc_info:
147 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
148 assert exc_info.value.status_code == 0
149
150 def test_get_with_no_body_sends_no_content(self) -> None:
151 client = _mock_httpx_client_resp(b'{"ok": true}')
152 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
153 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
154 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
155 sent_body = client.request.call_args.kwargs.get("content")
156 assert sent_body is None
157
158 def test_uses_httpx_client_not_urllib(self) -> None:
159 """hub_json must go through _httpx_mod.Client, not urllib."""
160 client = _mock_httpx_client_resp(b'{"ok": true}')
161 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
162 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
163 with unittest.mock.patch("muse.core.transport._open_url") as mock_urllib:
164 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
165 mock_mod.Client.assert_called_once()
166 mock_urllib.assert_not_called()
167
168
169 # ---------------------------------------------------------------------------
170 # hub_bytes
171 # ---------------------------------------------------------------------------
172
173
174 class TestHubBytes:
175 def test_returns_raw_bytes(self) -> None:
176 expected = b"# Muse Basics\nHello world"
177 client = _mock_httpx_client_resp(expected)
178 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
179 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
180 result = HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
181 assert result == expected
182
183 def test_sends_get_method(self) -> None:
184 client = _mock_httpx_client_resp(b"data")
185 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
186 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
187 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
188 method_called = client.request.call_args[0][0]
189 assert method_called == "GET"
190
191 def test_accept_star_header_sent(self) -> None:
192 client = _mock_httpx_client_resp(b"data")
193 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
194 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
195 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
196 headers = client.request.call_args.kwargs.get("headers", {})
197 accept = headers.get("Accept") or headers.get("accept", "")
198 assert accept == "*/*"
199
200 def test_msign_header_sent_when_signing_provided(self) -> None:
201 signing = _make_signing()
202 client = _mock_httpx_client_resp(b"data")
203 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
204 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
205 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", signing)
206 headers = client.request.call_args.kwargs.get("headers", {})
207 auth = headers.get("Authorization") or headers.get("authorization", "")
208 assert auth.startswith('MSign handle="testuser"')
209
210 def test_no_auth_header_when_signing_is_none(self) -> None:
211 client = _mock_httpx_client_resp(b"data")
212 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
213 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
214 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
215 headers = client.request.call_args.kwargs.get("headers", {})
216 auth = headers.get("Authorization") or headers.get("authorization")
217 assert auth is None
218
219 def test_http_404_raises_transport_error(self) -> None:
220 client = _mock_httpx_client_resp(b"Not Found", status=404)
221 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
222 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
223 with pytest.raises(TransportError) as exc_info:
224 HttpTransport().hub_bytes("https://hub.example.com/mist/missing/raw", None)
225 assert exc_info.value.status_code == 404
226
227 def test_network_error_raises_transport_error_with_code_0(self) -> None:
228 client = unittest.mock.MagicMock()
229 client.is_closed = False
230 client.request = unittest.mock.MagicMock(side_effect=Exception("timed out"))
231 client.__enter__ = unittest.mock.MagicMock(return_value=client)
232 client.__exit__ = unittest.mock.MagicMock(return_value=False)
233 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
234 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
235 with pytest.raises(TransportError) as exc_info:
236 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
237 assert exc_info.value.status_code == 0
238
239 def test_uses_httpx_client_not_urllib(self) -> None:
240 """hub_bytes must go through _httpx_mod.Client, not urllib."""
241 client = _mock_httpx_client_resp(b"data")
242 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
243 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
244 with unittest.mock.patch("muse.core.transport._open_url") as mock_urllib:
245 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
246 mock_mod.Client.assert_called_once()
247 mock_urllib.assert_not_called()
248
249 def test_returns_empty_bytes_on_empty_200_response(self) -> None:
250 client = _mock_httpx_client_resp(b"")
251 with unittest.mock.patch("muse.core.transport._httpx_mod") as mock_mod:
252 mock_mod.Client = unittest.mock.MagicMock(return_value=client)
253 result = HttpTransport().hub_bytes("https://hub.example.com/mist/empty/raw", None)
254 assert result == b""
File History 2 commits
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor 20 days ago
sha256:0bea7600d1eee83e87950be49933b1006fa9dc2c71e7c4ee748d324f61138156 chore: bump version to 0.2.0rc11; fix typing audit violatio… Sonnet 4.6 minor 20 days ago