aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_musicbrainz_config_policy.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 11:15:54 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 11:15:54 +0200
commitb7733e812fadd6007976d262bd1d793572a36ba7 (patch)
tree52e2ac07c9ba4929fa50b6ba8d19f94f86e8b304 /packages/meshbay-node/tests/test_musicbrainz_config_policy.py
parent09a007937f03fad04a390323f3817f1ae7d7c2a9 (diff)
downloadmeshbay-b7733e812fadd6007976d262bd1d793572a36ba7.tar.gz
feat: node workflow redesign — wizard auto-config, reset, MusicBrainz contact
Wizard (Electron): - Auto-provisions node config (hub URL + username) from logged-in user - node:start handles both cold start and restart of misconfigured daemon - Waits for daemon to reach 'running', auto-links node key on hub - probeNode accepts intermediate states for wizard progress feedback Reset (meshbay-node reset): - Unlinks node key from hub (DELETE /me/node_key, best-effort) - Stops and disables daemon (systemctl --user disable --now) - Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay MusicBrainz contact: - Resolved from owner's hub email instead of per-node roster config - Removed musicbrainz_contact UI and WebRTC handshake field - Removed set_musicbrainz_contact/musicbrainz_contact from roster Node pairing: - Added operator pairing banner on NodePage - Added operator_paired flag to list_groups Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_musicbrainz_config_policy.py')
-rw-r--r--packages/meshbay-node/tests/test_musicbrainz_config_policy.py179
1 files changed, 0 insertions, 179 deletions
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()