summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py10
-rw-r--r--packages/meshbay-node/tests/test_hub_ws_group_claim.py100
2 files changed, 108 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index 8873958..ef12bb4 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -319,9 +319,15 @@ class HubClient:
"token": self._session.access_token,
"node_id": self._session.node_id,
}
+ # `if gids:` here, and the hub read a missing key as "claims
+ # every group this account belongs to" — so a node hosting
+ # nothing advertised itself for all of them and swallowed
+ # their traffic. The hub no longer widens an absent claim,
+ # and this says the empty set out loud rather than by
+ # omission: hosting nothing is a fact, not a missing field.
gids = group_ids() if callable(group_ids) else group_ids
- if gids:
- auth_msg["group_ids"] = gids
+ if gids is not None:
+ auth_msg["group_ids"] = list(gids)
await ws.send(json.dumps(auth_msg))
# Bounded: a hub that accepts the socket and then says nothing
# — which is what it does for a few seconds while restarting —
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