""" Whether TMDB lookups run *at all* for a group — docs/MESHBAY_DESIGN.md §9.7. Per-group (2026-08-24 — used to be node-wide, folded into tmdb_config): a real media-library group and a test/demo group on the same node need not share the decision to spend TMDB quota and make outbound requests. Same shape as test_video_root_policy.py: a signed operator instruction, scoped to self._group_id (not passed explicitly on the wire), stored via roster.py's group_settings table under the real group_id. The custom API token and query language stay node-wide — see test_tmdb_config_policy.py for those. """ from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.adminop import OP_TMDB_ENABLED from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roster import Roster from meshbay_node.transport.webrtc_server import WebRTCPeerSession 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 = "g" * 32 session._user_id = user_id session._pk_user = "" session.sent = [] session._send = session.sent.append session._audit = lambda *a, **k: None return session # ── Refused before a challenge is even issued ─────────────────────────────── async def test_missing_enabled_is_refused(tmp_path): session = _session(tmp_path, "op", operator="op") session._has_admin_authority = lambda: True issued = [] session._issue_admin_challenge = lambda op, subject: issued.append((op, subject)) session._do_tmdb_enabled({}) assert not issued assert [m for m in session.sent if m.get("type") == "error"] async def test_non_bool_enabled_is_refused(tmp_path): session = _session(tmp_path, "op", operator="op") session._has_admin_authority = lambda: True issued = [] session._issue_admin_challenge = lambda op, subject: issued.append((op, subject)) session._do_tmdb_enabled({"enabled": "yes"}) 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_tmdb_enabled({"enabled": False}) assert [m for m in session.sent if m.get("type") == "error"] # ── Accepted cases ─────────────────────────────────────────────────────────── async def test_a_valid_request_is_signed_against_this_groups_id(tmp_path): session = _session(tmp_path, "op", operator="op") session._has_admin_authority = lambda: True issued = [] session._issue_admin_challenge = lambda op, subject: issued.append((op, subject)) session._do_tmdb_enabled({"enabled": True}) assert issued == [(OP_TMDB_ENABLED, "True")] async def test_disabling_is_signed_too(tmp_path): session = _session(tmp_path, "op", operator="op") session._has_admin_authority = lambda: True issued = [] session._issue_admin_challenge = lambda op, subject: issued.append((op, subject)) session._do_tmdb_enabled({"enabled": False}) assert issued == [(OP_TMDB_ENABLED, "False")] # ── 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.tmdb_enabled("g1") is True, "absent must mean on" await roster.set_tmdb_enabled("g1", False, set_by="op") assert await roster.tmdb_enabled("g1") is False finally: await roster.close() reopened = Roster(db_path=tmp_path / "roster.db") await reopened.open() try: assert await reopened.tmdb_enabled("g1") is False assert await reopened.tmdb_enabled("g2") is True, \ "one group's setting must not answer for another" finally: await reopened.close()