aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_hub_ws_group_claim.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_hub_ws_group_claim.py')
-rw-r--r--packages/meshbay-node/tests/test_hub_ws_group_claim.py100
1 files changed, 100 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_hub_ws_group_claim.py b/packages/meshbay-node/tests/test_hub_ws_group_claim.py
new file mode 100644
index 0000000..49ac776
--- /dev/null
+++ b/packages/meshbay-node/tests/test_hub_ws_group_claim.py
@@ -0,0 +1,100 @@
+"""
+What the node claims to host, on the wire.
+
+2026-09-11: the node omitted `group_ids` from its WS auth frame whenever it
+hosted nothing (`if gids:`), and the hub read an absent claim as "every group
+this account belongs to". An unconfigured node was therefore registered as a
+source for other people's groups, could serve none of them, and — being listed
+first — took them down for every member.
+
+The hub no longer widens an absent claim. This pins the node's half: the empty
+set is stated, not left to be inferred from a missing field.
+"""
+
+import asyncio
+import json
+
+import pytest
+
+from meshbay_node.hub_client import HubClient, HubConfig, HubSession
+
+
+class _FakeWS:
+ """Just enough of a websockets connection for one auth round trip."""
+
+ def __init__(self, sent: list, authed: asyncio.Event):
+ self._sent = sent
+ self._authed = authed
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *_):
+ return False
+
+ async def send(self, raw: str) -> None:
+ self._sent.append(json.loads(raw))
+
+ async def recv(self) -> str:
+ return json.dumps({"type": "auth_ok", "node_id": "node-1"})
+
+ def __aiter__(self):
+ return self
+
+ async def __anext__(self):
+ # Registered; nothing more to drive. Hold here rather than closing, so
+ # the reconnect path does not run and muddy what was sent.
+ self._authed.set()
+ await asyncio.sleep(3600)
+ raise StopAsyncIteration
+
+
+async def _auth_frame(monkeypatch, group_ids) -> dict:
+ import websockets
+
+ sent: list = []
+ authed = asyncio.Event()
+ monkeypatch.setattr(
+ websockets, "connect", lambda *a, **kw: _FakeWS(sent, authed))
+
+ client = HubClient(HubConfig(hub_url="https://hub.test", username="u"),
+ keys=None)
+ client._session = HubSession(
+ hub_url="https://hub.test", username="u", user_id="user-1",
+ access_token="tok", refresh_token="ref", hub_pk_pem=b"", node_id="node-1")
+
+ task = asyncio.create_task(client.maintain_ws(group_ids=group_ids))
+ try:
+ await asyncio.wait_for(authed.wait(), timeout=5)
+ finally:
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
+ await client.close()
+
+ assert sent, "no auth frame was sent"
+ return sent[0]
+
+
+@pytest.mark.asyncio
+async def test_a_node_hosting_nothing_says_so(monkeypatch):
+ """The empty set on the wire, rather than a key the hub has to interpret."""
+ frame = await _auth_frame(monkeypatch, lambda: [])
+ assert "group_ids" in frame, (
+ "an absent claim is what the hub used to read as 'all of them'")
+ assert frame["group_ids"] == []
+
+
+@pytest.mark.asyncio
+async def test_a_node_declares_the_groups_it_hosts(monkeypatch):
+ frame = await _auth_frame(monkeypatch, lambda: ["g-1", "g-2"])
+ assert frame["group_ids"] == ["g-1", "g-2"]
+
+
+@pytest.mark.asyncio
+async def test_a_caller_with_nothing_to_declare_sends_no_key(monkeypatch):
+ """`None` is not the same as `[]`: it is "I am not answering that question"."""
+ frame = await _auth_frame(monkeypatch, None)
+ assert "group_ids" not in frame