"""TDD tests for HttpTransport.hub_json and HttpTransport.hub_bytes.""" from __future__ import annotations import json import unittest.mock import pytest from muse.core.transport import HttpTransport, SigningIdentity, TransportError # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_signing() -> SigningIdentity: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey return SigningIdentity(handle="testuser", private_key=Ed25519PrivateKey.generate()) def _mock_urllib_do(body: bytes, status: int = 200) -> "Callable[..., bytes]": """Patch _urllib_do to return body or raise TransportError for non-2xx.""" calls: list[tuple[str, str, dict, bytes | None]] = [] def _side_effect(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes: calls.append((method, url, headers, data)) if status >= 400: raise TransportError(f"HTTP {status}", status) return body _side_effect.calls = calls return _side_effect # --------------------------------------------------------------------------- # hub_json # --------------------------------------------------------------------------- class TestHubJson: def test_returns_parsed_dict(self) -> None: payload = {"muse_version": "0.1.0", "mist_id": "abc123", "ok": True} do = _mock_urllib_do(json.dumps(payload).encode()) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): result = HttpTransport().hub_json("GET", "https://hub.example.com/mist/abc123", None) assert result == payload def test_post_with_body_sends_json_bytes(self) -> None: body = {"title": "hello", "content": "world"} do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body=body) _, _, _, sent_data = do.calls[0] assert sent_data == json.dumps(body).encode() def test_post_with_body_sets_content_type_json(self) -> None: do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body={"x": 1}) _, _, headers, _ = do.calls[0] ct = headers.get("Content-Type") or headers.get("content-type", "") assert "application/json" in ct def test_accept_json_header_sent(self) -> None: do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) _, _, headers, _ = do.calls[0] accept = headers.get("Accept") or headers.get("accept", "") assert "application/json" in accept def test_msign_auth_header_sent_when_signing_provided(self) -> None: signing = _make_signing() do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with unittest.mock.patch("muse.core.hub_trust.check_and_pin"): HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", signing) _, _, headers, _ = do.calls[0] auth = headers.get("Authorization") or headers.get("authorization", "") assert auth.startswith('MSign handle="testuser"') def test_no_auth_header_when_signing_is_none(self) -> None: do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) _, _, headers, _ = do.calls[0] auth = headers.get("Authorization") or headers.get("authorization") assert auth is None def test_non_dict_json_response_returns_empty_dict(self) -> None: do = _mock_urllib_do(b'["a", "b", "c"]') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): result = HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) assert result == {} def test_invalid_json_raises_transport_error(self) -> None: do = _mock_urllib_do(b"not json at all") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) assert "invalid json" in str(exc_info.value).lower() def test_http_404_raises_transport_error(self) -> None: do = _mock_urllib_do(b"Not Found", status=404) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_json("GET", "https://hub.example.com/mist/missing", None) assert exc_info.value.status_code == 404 def test_http_401_raises_transport_error(self) -> None: do = _mock_urllib_do(b"Unauthorized", status=401) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_json("POST", "https://hub.example.com/mists", None, body={}) assert exc_info.value.status_code == 401 def test_http_500_raises_transport_error(self) -> None: do = _mock_urllib_do(b"Server Error", status=500) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) assert exc_info.value.status_code == 500 def test_network_error_raises_transport_error_with_code_0(self) -> None: def _fail(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes: raise TransportError("connection refused", 0) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=_fail): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) assert exc_info.value.status_code == 0 def test_get_with_no_body_sends_no_content(self) -> None: do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) _, _, _, sent_data = do.calls[0] assert sent_data is None def test_uses_urllib_not_open_url(self) -> None: """hub_json must go through _urllib_do, not _open_url (the fetch seam).""" do = _mock_urllib_do(b'{"ok": true}') with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do) as mock_do: with unittest.mock.patch("muse.core.transport._open_url") as mock_open: HttpTransport().hub_json("GET", "https://hub.example.com/mist/x", None) assert mock_do.called mock_open.assert_not_called() # --------------------------------------------------------------------------- # hub_bytes # --------------------------------------------------------------------------- class TestHubBytes: def test_returns_raw_bytes(self) -> None: expected = b"# Muse Basics\nHello world" do = _mock_urllib_do(expected) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): result = HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) assert result == expected def test_sends_get_method(self) -> None: do = _mock_urllib_do(b"data") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) method, _, _, _ = do.calls[0] assert method == "GET" def test_accept_star_header_sent(self) -> None: do = _mock_urllib_do(b"data") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) _, _, headers, _ = do.calls[0] accept = headers.get("Accept") or headers.get("accept", "") assert accept == "*/*" def test_msign_header_sent_when_signing_provided(self) -> None: signing = _make_signing() do = _mock_urllib_do(b"data") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with unittest.mock.patch("muse.core.hub_trust.check_and_pin"): HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", signing) _, _, headers, _ = do.calls[0] auth = headers.get("Authorization") or headers.get("authorization", "") assert auth.startswith('MSign handle="testuser"') def test_no_auth_header_when_signing_is_none(self) -> None: do = _mock_urllib_do(b"data") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) _, _, headers, _ = do.calls[0] auth = headers.get("Authorization") or headers.get("authorization") assert auth is None def test_http_404_raises_transport_error(self) -> None: do = _mock_urllib_do(b"Not Found", status=404) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_bytes("https://hub.example.com/mist/missing/raw", None) assert exc_info.value.status_code == 404 def test_network_error_raises_transport_error_with_code_0(self) -> None: def _fail(method: str, url: str, headers: "dict[str, str]", data: "bytes | None" = None, **kwargs: "str | int | bool") -> bytes: raise TransportError("timed out", 0) with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=_fail): with pytest.raises(TransportError) as exc_info: HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) assert exc_info.value.status_code == 0 def test_uses_urllib_not_open_url(self) -> None: """hub_bytes must go through _urllib_do, not _open_url (the fetch seam).""" do = _mock_urllib_do(b"data") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do) as mock_do: with unittest.mock.patch("muse.core.transport._open_url") as mock_open: HttpTransport().hub_bytes("https://hub.example.com/mist/abc/raw", None) assert mock_do.called mock_open.assert_not_called() def test_returns_empty_bytes_on_empty_200_response(self) -> None: do = _mock_urllib_do(b"") with unittest.mock.patch("muse.core.transport._urllib_do", side_effect=do): result = HttpTransport().hub_bytes("https://hub.example.com/mist/empty/raw", None) assert result == b""