aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_offer_retry.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_offer_retry.py')
-rw-r--r--packages/meshbay-hub/tests/test_offer_retry.py78
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]