diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_offer_retry.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_offer_retry.py | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_offer_retry.py b/packages/meshbay-hub/tests/test_offer_retry.py new file mode 100644 index 0000000..3c968db --- /dev/null +++ b/packages/meshbay-hub/tests/test_offer_retry.py @@ -0,0 +1,78 @@ +""" +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 + +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), str(TRANSPORT), 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] |