1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
"""
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
|