aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_offer_retry.py
blob: 6389f8e20e07f63b4a4a823d0984cb1d3a08ee69 (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
"""
An offer the hub refuses for load is sent again; one refused for cause is not.

A phone's Search reported groups as unreachable whose node was answering every
offer that reached it: the hub had refused those offers with 429, one of its
per-account ceilings, and the browser took that for the node. A 429 — or a 502
or 503 while the hub restarts behind its proxy — says the hub is busy, so
`postOffer` waits (the hub's `Retry-After` when it gives one) and sends the same
offer again. A 404 for a node that is not connected, or a 504 for one that did
not answer, fails at once: retrying those would make a dead node cost time.

These run the shipped `postOffer`, lifted out of transport.js as text, against
a fake hub and a fake clock — see harness/offer_retry_harness.mjs.
"""

import json
import shutil
import subprocess
from pathlib import Path

import pytest
from spa_source import transport_argv

STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
TRANSPORT = STATIC / "transport.js"
HARNESS = Path(__file__).parent / "harness" / "offer_retry_harness.mjs"

pytestmark = pytest.mark.skipif(
    shutil.which("node") is None or not TRANSPORT.exists(),
    reason="node or the SPA sources are not available")


def _post(**cfg) -> dict:
    proc = subprocess.run(
        ["node", str(HARNESS), transport_argv(), json.dumps(cfg)],
        capture_output=True, text=True)
    assert proc.returncode == 0, proc.stderr
    return json.loads(proc.stdout)


def test_a_busy_hub_is_asked_again_when_it_says():
    out = _post(answers=[[429, 1], 200])
    assert out["result"] == "answered"
    assert out["posts"] == [0, 1000], "Retry-After was not honoured"


def test_without_retry_after_the_waits_grow():
    out = _post(answers=[429, 429, 503, 200])
    assert out["result"] == "answered"
    assert out["posts"] == [0, 500, 1500, 3500]


def test_a_node_that_is_not_there_fails_at_once():
    for status in (404, 403, 504):
        out = _post(answers=[status])
        assert out["result"] == "failed"
        assert out["status"] == status
        assert out["posts"] == [0], f"{status} was retried"


def test_a_hub_that_stays_busy_is_given_up_on():
    out = _post(answers=[429])
    assert out["result"] == "failed"
    assert out["status"] == 429
    assert len(out["posts"]) == 6
    assert out["at"] <= 20000, "a busy hub cost more than the worst case stated"


def test_a_retry_after_that_asks_too_much_is_capped():
    out = _post(answers=[[429, 3600], 200])
    assert out["posts"] == [0, 10000]


def test_a_caller_that_gave_up_sends_nothing_more():
    """Search's deadline, or a page that went away, closes the transport while
    it waits; the offer must not go out on its behalf afterwards."""
    out = _post(answers=[429], closeAt=100)
    assert out["result"] == "failed"
    assert out["posts"] == [0]