summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_musicbrainz.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_musicbrainz.py')
-rw-r--r--packages/meshbay-node/tests/test_musicbrainz.py163
1 files changed, 163 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_musicbrainz.py b/packages/meshbay-node/tests/test_musicbrainz.py
new file mode 100644
index 0000000..482395d
--- /dev/null
+++ b/packages/meshbay-node/tests/test_musicbrainz.py
@@ -0,0 +1,163 @@
+"""Tests for musicbrainz.py against a mocked httpx transport — no live network in CI."""
+
+import time
+
+import httpx
+import pytest
+
+from meshbay_node.musicbrainz import MusicBrainzClient, _MIN_INTERVAL_SECS
+
+
+class FakeRoster:
+ def __init__(self, contact: str | None = "operator@example.invalid"):
+ self._contact = contact
+
+ async def musicbrainz_contact(self):
+ return self._contact
+
+
+def _handler(response_map):
+ def handle(request: httpx.Request) -> httpx.Response:
+ path = request.url.path
+ for prefix, body in response_map.items():
+ if path.endswith(prefix):
+ return httpx.Response(200, json=body)
+ return httpx.Response(404, json={})
+ return handle
+
+
+@pytest.mark.asyncio
+async def test_search_release_returns_top_result_and_confidence():
+ body = {"releases": [{"id": "abc-123", "title": "The Great Album",
+ "artist-credit": [{"name": "Some Artist"}]}]}
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(_handler({"release": body})),
+ )
+ result, ratio = await client.search_release("Some Artist", "The Great Album")
+
+ assert result is not None
+ assert result["id"] == "abc-123"
+ assert ratio > 0.9
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_no_results_returns_none_and_zero_confidence():
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(_handler({"release": {"releases": []}})),
+ )
+ result, ratio = await client.search_release("Nobody", "Nonexistent Obscure Album")
+
+ assert result is None
+ assert ratio == 0.0
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_no_contact_configured_makes_no_request(monkeypatch):
+ monkeypatch.delenv("MESHBAY_MUSICBRAINZ_CONTACT_DEFAULT", raising=False)
+ calls = []
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ calls.append(request)
+ return httpx.Response(200, json={"releases": []})
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(contact=None),
+ transport=httpx.MockTransport(handle),
+ )
+ result, ratio = await client.search_release("Anyone", "Anything")
+
+ assert result is None
+ assert calls == [], "an unidentified client must never be sent — see musicbay.md §3.1"
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_the_configured_contact_is_sent_as_user_agent():
+ captured = {}
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ captured["ua"] = request.headers.get("user-agent")
+ return httpx.Response(200, json={"releases": []})
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(contact="operator@example.invalid"),
+ transport=httpx.MockTransport(handle),
+ )
+ await client.search_release("Anyone", "Anything")
+
+ assert "operator@example.invalid" in captured["ua"]
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_http_error_returns_none_gracefully():
+ def handle(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(500, json={"error": "server error"})
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(handle),
+ )
+ result, ratio = await client.search_release("Anyone", "Anything")
+
+ assert result is None
+ assert ratio == 0.0
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_cover_art_missing_returns_none_not_an_error():
+ def handle(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(404)
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(handle),
+ )
+ content = await client.fetch_cover_art("abc-123")
+
+ assert content is None
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_cover_art_found_returns_bytes():
+ def handle(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, content=b"\xff\xd8fake-jpeg-bytes")
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(handle),
+ )
+ content = await client.fetch_cover_art("abc-123")
+
+ assert content == b"\xff\xd8fake-jpeg-bytes"
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_calls_are_paced_at_least_min_interval_apart():
+ """
+ docs/musicbay.md §3.2: the ~1 req/s courtesy limit is this node's own
+ job, not something the server hands out — verified by timing two calls
+ back to back rather than mocking the clock, so a change to the pacing
+ implementation that still meets the contract doesn't break this test.
+ """
+ def handle(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, json={"releases": []})
+
+ client = MusicBrainzClient(
+ roster=FakeRoster(),
+ transport=httpx.MockTransport(handle),
+ )
+ start = time.monotonic()
+ await client.search_release("A", "One")
+ await client.search_release("B", "Two")
+ elapsed = time.monotonic() - start
+
+ assert elapsed >= _MIN_INTERVAL_SECS * 0.9
+ await client.close()