summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_hub_ws_group_claim.py
blob: 49ac7764ff84b453658d77dedac3a19107853972 (plain) (blame)
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
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