gabriel / muse public

test_transport_hub_json.py file-level

at sha256:7 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:e fix: get_signing_identity's MUSE_AGENT_KEY_FD path derived with entity_… · gabriel · Oct 22, 2026
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
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_urllib_do(body: bytes, status: int = 200) -> "Callable[..., bytes]":
24 """Patch _urllib_do to return body or raise TransportError for non-2xx."""
25 calls: list[tuple[str, str, dict, bytes | None]] = []
26
27 def _side_effect(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes:
28 calls.append((method, url, headers, data))
29 if status >= 400:
30 raise TransportError(f"HTTP {status}", status)
31 return body
32
33 _side_effect.calls = calls
34 return _side_effect
35
36
37 # ---------------------------------------------------------------------------
38 # hub_json
39 # ---------------------------------------------------------------------------
40
41
42 class TestHubJson:
43 def test_returns_parsed_dict(self) -> None:
44 payload = {"muse_version": "0.1.0", "mist_id": "abc123", "ok": True}
45 do = _mock_urllib_do(json.dumps(payload).encode())
46 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
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 body = {"title": "hello", "content": "world"}
52 do = _mock_urllib_do(b'{"ok": true}')
53 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
54 HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body=body)
55 _, _, _, sent_data = do.calls[0]
56 assert sent_data == json.dumps(body).encode()
57
58 def test_post_with_body_sets_content_type_json(self) -> None:
59 do = _mock_urllib_do(b'{"ok": true}')
60 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
61 HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body={"x": 1})
62 _, _, headers, _ = do.calls[0]
63 ct = headers.get("Content-Type") or headers.get("content-type", "")
64 assert "application/json" in ct
65
66 def test_accept_json_header_sent(self) -> None:
67 do = _mock_urllib_do(b'{"ok": true}')
68 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
69 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
70 _, _, headers, _ = do.calls[0]
71 accept = headers.get("Accept") or headers.get("accept", "")
72 assert "application/json" in accept
73
74 def test_msign_auth_header_sent_when_signing_provided(self) -> None:
75 signing = _make_signing()
76 do = _mock_urllib_do(b'{"ok": true}')
77 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
78 with unittest.mock.patch("muse.core.hub_trust.check_and_pin"):
79 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", signing)
80 _, _, headers, _ = do.calls[0]
81 auth = headers.get("Authorization") or headers.get("authorization", "")
82 assert auth.startswith('MSign handle="testuser"')
83
84 def test_no_auth_header_when_signing_is_none(self) -> None:
85 do = _mock_urllib_do(b'{"ok": true}')
86 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
87 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
88 _, _, headers, _ = do.calls[0]
89 auth = headers.get("Authorization") or headers.get("authorization")
90 assert auth is None
91
92 def test_list_json_response_returned_as_list(self) -> None:
93 """musehub#213: a genuine top-level JSON array (e.g. GET .../forks)
94 must pass through unchanged, not be silently coerced to {}."""
95 do = _mock_urllib_do(b'["a", "b", "c"]')
96 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
97 result = HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
98 assert result == ["a", "b", "c"]
99
100 def test_scalar_json_response_raises_transport_error(self) -> None:
101 """A top-level JSON scalar (not an object or array) is a genuinely
102 unexpected shape no caller handles — raise rather than silently
103 emptying it, same as invalid JSON."""
104 do = _mock_urllib_do(b'"just a string"')
105 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
106 with pytest.raises(TransportError) as exc_info:
107 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
108 assert "unexpected json shape" in str(exc_info.value).lower()
109
110 def test_invalid_json_raises_transport_error(self) -> None:
111 do = _mock_urllib_do(b"not json at all")
112 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
113 with pytest.raises(TransportError) as exc_info:
114 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
115 assert "invalid json" in str(exc_info.value).lower()
116
117 def test_http_404_raises_transport_error(self) -> None:
118 do = _mock_urllib_do(b"Not Found", status=404)
119 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
120 with pytest.raises(TransportError) as exc_info:
121 HttpTransport().hub_json("GET", "https://hub.example.com/mist/missing", None)
122 assert exc_info.value.status_code == 404
123
124 def test_http_401_raises_transport_error(self) -> None:
125 do = _mock_urllib_do(b"Unauthorized", status=401)
126 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
127 with pytest.raises(TransportError) as exc_info:
128 HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body={})
129 assert exc_info.value.status_code == 401
130
131 def test_http_500_raises_transport_error(self) -> None:
132 do = _mock_urllib_do(b"Server Error", status=500)
133 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
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 def _fail(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes:
140 raise TransportError("connection refused", 0)
141 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=_fail):
142 with pytest.raises(TransportError) as exc_info:
143 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
144 assert exc_info.value.status_code == 0
145
146 def test_get_with_no_body_sends_no_content(self) -> None:
147 do = _mock_urllib_do(b'{"ok": true}')
148 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
149 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
150 _, _, _, sent_data = do.calls[0]
151 assert sent_data is None
152
153 def test_uses_urllib_not_open_url(self) -> None:
154 """hub_json must go through _urllib_do, not _open_url (the fetch seam)."""
155 do = _mock_urllib_do(b'{"ok": true}')
156 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do) as mock_do:
157 with unittest.mock.patch("muse.core.transport._open_url") as mock_open:
158 HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None)
159 assert mock_do.called
160 mock_open.assert_not_called()
161
162
163 # ---------------------------------------------------------------------------
164 # hub_bytes
165 # ---------------------------------------------------------------------------
166
167
168 class TestHubBytes:
169 def test_returns_raw_bytes(self) -> None:
170 expected = b"# Muse Basics\nHello world"
171 do = _mock_urllib_do(expected)
172 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
173 result = HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
174 assert result == expected
175
176 def test_sends_get_method(self) -> None:
177 do = _mock_urllib_do(b"data")
178 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
179 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
180 method, _, _, _ = do.calls[0]
181 assert method == "GET"
182
183 def test_accept_star_header_sent(self) -> None:
184 do = _mock_urllib_do(b"data")
185 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
186 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
187 _, _, headers, _ = do.calls[0]
188 accept = headers.get("Accept") or headers.get("accept", "")
189 assert accept == "*/*"
190
191 def test_msign_header_sent_when_signing_provided(self) -> None:
192 signing = _make_signing()
193 do = _mock_urllib_do(b"data")
194 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
195 with unittest.mock.patch("muse.core.hub_trust.check_and_pin"):
196 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", signing)
197 _, _, headers, _ = do.calls[0]
198 auth = headers.get("Authorization") or headers.get("authorization", "")
199 assert auth.startswith('MSign handle="testuser"')
200
201 def test_no_auth_header_when_signing_is_none(self) -> None:
202 do = _mock_urllib_do(b"data")
203 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
204 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
205 _, _, headers, _ = do.calls[0]
206 auth = headers.get("Authorization") or headers.get("authorization")
207 assert auth is None
208
209 def test_http_404_raises_transport_error(self) -> None:
210 do = _mock_urllib_do(b"Not Found", status=404)
211 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
212 with pytest.raises(TransportError) as exc_info:
213 HttpTransport().hub_bytes("https://hub.example.com/mist/missing/raw", None)
214 assert exc_info.value.status_code == 404
215
216 def test_network_error_raises_transport_error_with_code_0(self) -> None:
217 def _fail(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes:
218 raise TransportError("timed out", 0)
219 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=_fail):
220 with pytest.raises(TransportError) as exc_info:
221 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
222 assert exc_info.value.status_code == 0
223
224 def test_uses_urllib_not_open_url(self) -> None:
225 """hub_bytes must go through _urllib_do, not _open_url (the fetch seam)."""
226 do = _mock_urllib_do(b"data")
227 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do) as mock_do:
228 with unittest.mock.patch("muse.core.transport._open_url") as mock_open:
229 HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None)
230 assert mock_do.called
231 mock_open.assert_not_called()
232
233 def test_returns_empty_bytes_on_empty_200_response(self) -> None:
234 do = _mock_urllib_do(b"")
235 with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do):
236 result = HttpTransport().hub_bytes("https://hub.example.com/mist/empty/raw", None)
237 assert result == b""