diff options
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_cli_dispatch.py | 2 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_musicbrainz.py | 33 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_musicbrainz_config_policy.py | 179 |
3 files changed, 13 insertions, 201 deletions
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py index d9edb17..aff7330 100644 --- a/packages/meshbay-node/tests/test_cli_dispatch.py +++ b/packages/meshbay-node/tests/test_cli_dispatch.py @@ -128,7 +128,7 @@ def test_the_verb_list_here_matches_the_parser(): exercised = {argv[0] for argv in VERBS} # `init` writes a config file and `calibrate-argon2` burns CPU for seconds; # both are excluded on purpose rather than by omission. - untested = declared - exercised - {"init", "calibrate-argon2"} + untested = declared - exercised - {"init", "reset", "calibrate-argon2"} assert not untested, ( f"CLI verbs with no dispatch test: {sorted(untested)} — add them to " f"VERBS above") diff --git a/packages/meshbay-node/tests/test_musicbrainz.py b/packages/meshbay-node/tests/test_musicbrainz.py index 4d075d3..843a691 100644 --- a/packages/meshbay-node/tests/test_musicbrainz.py +++ b/packages/meshbay-node/tests/test_musicbrainz.py @@ -7,14 +7,6 @@ import pytest from meshbay_node.musicbrainz import _MIN_INTERVAL_SECS, MusicBrainzClient, _escape_lucene -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 @@ -30,7 +22,7 @@ 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(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(_handler({"release": body})), ) result, ratio = await client.search_release("Some Artist", "The Great Album") @@ -44,7 +36,7 @@ async def test_search_release_returns_top_result_and_confidence(): @pytest.mark.asyncio async def test_no_results_returns_none_and_zero_confidence(): client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(_handler({"release": {"releases": []}})), ) result, ratio = await client.search_release("Nobody", "Nonexistent Obscure Album") @@ -80,7 +72,7 @@ async def test_strict_match_does_not_pay_for_a_second_request(): }) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) await client.search_release("Some Artist", "The Great Album") @@ -112,7 +104,7 @@ async def test_falls_back_to_a_loose_query_when_the_strict_one_finds_nothing(): }) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) result, ratio = await client.search_release("Groundation", "Hebron Gate (2003)") @@ -139,7 +131,7 @@ async def test_confidence_reflects_a_wrong_artist_on_a_same_titled_release(): }) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) result, ratio = await client.search_release("Groundation", "Live") @@ -150,8 +142,7 @@ async def test_confidence_reflects_a_wrong_artist_on_a_same_titled_release(): @pytest.mark.asyncio -async def test_no_contact_configured_makes_no_request(monkeypatch): - monkeypatch.delenv("MESHBAY_MUSICBRAINZ_CONTACT_DEFAULT", raising=False) +async def test_no_contact_configured_makes_no_request(): calls = [] def handle(request: httpx.Request) -> httpx.Response: @@ -159,7 +150,7 @@ async def test_no_contact_configured_makes_no_request(monkeypatch): return httpx.Response(200, json={"releases": []}) client = MusicBrainzClient( - roster=FakeRoster(contact=None), + owner_email="", transport=httpx.MockTransport(handle), ) result, ratio = await client.search_release("Anyone", "Anything") @@ -178,7 +169,7 @@ async def test_the_configured_contact_is_sent_as_user_agent(): return httpx.Response(200, json={"releases": []}) client = MusicBrainzClient( - roster=FakeRoster(contact="operator@example.invalid"), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) await client.search_release("Anyone", "Anything") @@ -193,7 +184,7 @@ async def test_http_error_returns_none_gracefully(): return httpx.Response(500, json={"error": "server error"}) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) result, ratio = await client.search_release("Anyone", "Anything") @@ -209,7 +200,7 @@ async def test_cover_art_missing_returns_none_not_an_error(): return httpx.Response(404) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) content = await client.fetch_cover_art("abc-123") @@ -224,7 +215,7 @@ async def test_cover_art_found_returns_bytes(): return httpx.Response(200, content=b"\xff\xd8fake-jpeg-bytes") client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) content = await client.fetch_cover_art("abc-123") @@ -245,7 +236,7 @@ async def test_calls_are_paced_at_least_min_interval_apart(): return httpx.Response(200, json={"releases": []}) client = MusicBrainzClient( - roster=FakeRoster(), + owner_email="operator@example.invalid", transport=httpx.MockTransport(handle), ) start = time.monotonic() diff --git a/packages/meshbay-node/tests/test_musicbrainz_config_policy.py b/packages/meshbay-node/tests/test_musicbrainz_config_policy.py deleted file mode 100644 index 3590f01..0000000 --- a/packages/meshbay-node/tests/test_musicbrainz_config_policy.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -The operator's MusicBrainz User-Agent contact string — docs/musicbay.md -§3.2/§6. Same shape as test_tmdb_config_policy.py: a signed operator -instruction, node-wide (group_id="") rather than per-group, stored via -roster.py's group_settings table. - -Unlike TMDB's token, a contact string is not a secret — MusicBrainz's usage -policy expects it to be visible to the service it's sent to — but the -subject signed/audited still only ever says whether one was configured -(never the address itself), the same "yes/no" shape as tmdb_config's -subject, to keep a personal contact out of the audit log as free text. -""" - -from pathlib import Path - -import pytest - -from meshbay_common.adminop import OP_MUSICBRAINZ_CONFIG -from meshbay_node.indexer.group_index import GroupIndex -from meshbay_node.roster import Roster -from meshbay_node.transport.webrtc_server import WebRTCPeerSession -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from conftest import one_root - -pytestmark = pytest.mark.asyncio - - -def _session(tmp_path: Path, user_id: str, *, operator: str | None = None) -> WebRTCPeerSession: - shared_root = tmp_path / "shared" - shared_root.mkdir(exist_ok=True) - index = GroupIndex(group_id="g" * 32, sk_node=Ed25519PrivateKey.generate()) - ctx = { - "roots": one_root(shared_root), - "index": index, - "sk_node": index.sk_node, - "node_user_id": operator, - } - session = WebRTCPeerSession.__new__(WebRTCPeerSession) - session._ctx = ctx - session._group_id = None - session._user_id = user_id - session._pk_user = "" - session.sent = [] - session._send = session.sent.append - session._audit = lambda *a, **k: None - return session - - -def _fake_challenge(issued: list): - return lambda op, subject, payload=None, group_id=None: issued.append( - (op, subject, payload, group_id)) - - -# ── Refused before a challenge is even issued ─────────────────────────────── - -async def test_non_string_contact_is_refused(tmp_path): - session = _session(tmp_path, "op", operator="op") - session._has_admin_authority = lambda: True - issued = [] - session._issue_admin_challenge = _fake_challenge(issued) - - session._do_musicbrainz_config({"contact": 12345}) - - assert not issued - assert [m for m in session.sent if m.get("type") == "error"] - - -async def test_a_request_with_nobody_to_authorize_it_is_refused(tmp_path): - session = _session(tmp_path, "member-1", operator="the-operator") - session._has_admin_authority = lambda: False - - session._do_musicbrainz_config({"contact": "https://example.invalid/contact"}) - - assert [m for m in session.sent if m.get("type") == "error"] - - -# ── Who may change it, and what gets signed ───────────────────────────────── - -async def test_changing_it_needs_a_signature(tmp_path): - session = _session(tmp_path, "op", operator="op") - session._has_admin_authority = lambda: True - issued = [] - session._issue_admin_challenge = _fake_challenge(issued) - - session._do_musicbrainz_config({}) - - assert len(issued) == 1 - op, subject, payload, group_id = issued[0] - assert op == OP_MUSICBRAINZ_CONFIG - assert group_id == "", "node-wide, like tmdb_config — not tied to self._group_id" - - -async def test_the_contact_itself_never_appears_in_the_signed_subject(tmp_path): - """ - Not a secret the way a TMDB token is, but still kept out of the audited - subject line as free text — same "yes/no configured" shape. - """ - session = _session(tmp_path, "op", operator="op") - session._has_admin_authority = lambda: True - issued = [] - session._issue_admin_challenge = _fake_challenge(issued) - - contact = "operator@example.invalid" - session._do_musicbrainz_config({"contact": contact}) - - _, subject, payload, _ = issued[0] - assert contact not in subject - assert payload["contact"] == contact, "the real value still has to reach the exec step somehow" - - -async def test_subject_reflects_whether_a_contact_was_supplied(tmp_path): - session = _session(tmp_path, "op", operator="op") - session._has_admin_authority = lambda: True - issued = [] - session._issue_admin_challenge = _fake_challenge(issued) - - session._do_musicbrainz_config({"contact": "x"}) - - _, subject, _, _ = issued[0] - assert subject == "contact_configured=yes" - - -async def test_subject_says_no_contact_when_none_given(tmp_path): - session = _session(tmp_path, "op", operator="op") - session._has_admin_authority = lambda: True - issued = [] - session._issue_admin_challenge = _fake_challenge(issued) - - session._do_musicbrainz_config({}) - - _, subject, _, _ = issued[0] - assert subject == "contact_configured=no" - - -# ── Where it is stored ────────────────────────────────────────────────────── - -async def test_the_setting_lives_on_the_node_and_survives_a_restart(tmp_path): - roster = Roster(db_path=tmp_path / "roster.db") - await roster.open() - try: - assert await roster.musicbrainz_contact() is None, \ - "absent must mean 'no contact configured' — no shipped default to fall back to" - await roster.set_musicbrainz_contact("operator@example.invalid", set_by="op") - assert await roster.musicbrainz_contact() == "operator@example.invalid" - finally: - await roster.close() - - reopened = Roster(db_path=tmp_path / "roster.db") - await reopened.open() - try: - assert await reopened.musicbrainz_contact() == "operator@example.invalid" - finally: - await reopened.close() - - -async def test_clearing_the_contact_reverts_to_unconfigured(tmp_path): - roster = Roster(db_path=tmp_path / "roster.db") - await roster.open() - try: - await roster.set_musicbrainz_contact("a-contact", set_by="op") - assert await roster.musicbrainz_contact() == "a-contact" - - await roster.set_musicbrainz_contact("", set_by="op") - assert await roster.musicbrainz_contact() is None, \ - "an explicit empty string clears the contact" - finally: - await roster.close() - - -async def test_omitting_the_contact_leaves_it_unchanged(tmp_path): - roster = Roster(db_path=tmp_path / "roster.db") - await roster.open() - try: - await roster.set_musicbrainz_contact("a-contact", set_by="op") - await roster.set_musicbrainz_contact(None, set_by="op") - assert await roster.musicbrainz_contact() == "a-contact" - finally: - await roster.close() |