aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_connect_never_hangs.py
blob: 5680877e65a622c3e737fc264cfa7fcd3295c683 (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
"""
Joining a group must fail, or succeed — never wait forever.

Reported: a first attempt to connect hung with nothing on screen, and the same
account connected a few minutes later. That is the shape of a network wait with
no deadline, not of a refusal, and there were two of them.

**ICE gathering.** Signaling here is non-trickle — the offer carries its
candidates, so it is not sent until gathering says it is done. A STUN server
that is slow, filtered, or resolved through a DNS that is not answering means
`icegatheringstatechange` never reaches `complete`, and `connect()` never
returns. Same shape as the `fullscreen` denial: a promise that never settles
produces no error to find.

**The hub call in the desktop client.** Node's `fetch` has no default timeout,
so a host that accepts a connection and then says nothing holds the request for
as long as the OS allows. `hub:probe` had a deadline; `hub:fetch`, which carries
signaling, did not.
"""

import re
from pathlib import Path

import pytest
from spa_source import transport_source

STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
TRANSPORT = STATIC / "transport.js"
MAIN = (Path(__file__).resolve().parents[2] / "meshbay-client" / "src" / "main.js")

pytestmark = pytest.mark.skipif(not TRANSPORT.exists(),
                                reason="SPA sources unavailable")


def _gathering_block() -> str:
    """The wait on ICE gathering, and only it."""
    source = transport_source()
    start = source.index("iceGatheringState")
    return source[max(0, start - 900):start + 700]


def test_ice_gathering_has_a_deadline():
    block = _gathering_block()
    assert "setTimeout" in block, (
        "the wait on ICE gathering can never end, and connect() with it")
    assert "ICE_GATHER_TIMEOUT_MS" in block


def test_the_deadline_is_long_enough_for_a_stun_round_trip():
    """Cutting gathering off too early drops the reflexive candidate and breaks
    every connection that is not on the same network."""
    source = transport_source()
    match = re.search(r"const ICE_GATHER_TIMEOUT_MS = (\d+);", source)
    assert match, "the constant is gone or was renamed"
    assert 2000 <= int(match.group(1)) <= 10000


def test_a_timed_out_gathering_still_sends_the_offer():
    """Host candidates are already gathered, which is enough on a LAN. Giving
    up instead would turn a slow STUN server into a refusal to connect."""
    source = transport_source()
    block = _gathering_block()
    # The deadline resolves the promise; it does not reject it.
    assert "reject" not in block.split("setTimeout", 1)[1][:300]
    # And the offer is still posted afterwards.
    assert "webrtc/offer" in source[source.index("iceGatheringState"):]


@pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present")
def test_every_hub_call_from_the_client_has_a_deadline():
    source = MAIN.read_text(encoding="utf-8")
    block = source.split("ipcMain.handle('hub:fetch'", 1)[1].split("ipcMain.handle", 1)[0]
    assert "AbortSignal.timeout" in block, (
        "a hub that accepts the connection and says nothing holds this for "
        "as long as the OS allows")


@pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present")
def test_the_deadline_outlasts_the_hubs_own_longest_call():
    """Signaling waits fifteen seconds for a node to answer an offer. A client
    deadline under that would abort calls that were about to succeed."""
    source = MAIN.read_text(encoding="utf-8")
    match = re.search(r"const HUB_FETCH_TIMEOUT_MS = (\d+);", source)
    assert match, "the constant is gone or was renamed"
    assert int(match.group(1)) > 15000


@pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present")
def test_a_timeout_says_so_rather_than_saying_fetch_failed():
    source = MAIN.read_text(encoding="utf-8")
    block = source.split("ipcMain.handle('hub:fetch'", 1)[1].split("ipcMain.handle", 1)[0]
    assert "TimeoutError" in block and "did not answer" in block