diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_connect_never_hangs.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_connect_never_hangs.py | 91 |
1 files changed, 91 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_connect_never_hangs.py b/packages/meshbay-hub/tests/test_connect_never_hangs.py new file mode 100644 index 0000000..d6f1369 --- /dev/null +++ b/packages/meshbay-hub/tests/test_connect_never_hangs.py @@ -0,0 +1,91 @@ +""" +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 + +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.read_text(encoding="utf-8") + 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.read_text(encoding="utf-8") + 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.read_text(encoding="utf-8") + 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 |