import json from edge.events import Event, EventType, Severity from edge.alert import sign, PostClient, SendResult def _event(): return Event(EventType.FALL, Severity.HIGH, "cam-01", 7, None, 1000.0, 3.0, {"torso_angle": 78.0}, None) class _FakeResp: def __init__(self, code): self.status_code = code def test_sign_is_deterministic(): s1 = sign("secret", "n1", "1000", b"body") s2 = sign("secret", "n1", "1000", b"body") assert s1 == s2 and len(s1) == 64 def test_send_posts_signed_payload(): captured = {} def fake_post(url, data=None, headers=None, timeout=None, **kw): captured["url"] = url captured["headers"] = headers captured["body"] = data captured["kw"] = kw return _FakeResp(200) client = PostClient("https://x/api/events", "pi-01", "secret", nonce_fn=lambda: "n1", now_fn=lambda: 1000.0, http_post=fake_post) ok = client.send(_event(), [b"\xff\xd8jpeg"]) assert ok is SendResult.DELIVERED assert captured["headers"]["X-Nonce"] == "n1" assert captured["headers"]["X-Timestamp"] == "1000.0" payload = json.loads(captured["body"]) assert payload["device_id"] == "pi-01" assert payload["event_type"] == "fall" assert len(payload["snapshots"]) == 1 expected = sign("secret", "n1", "1000.0", captured["body"].encode()) assert captured["headers"]["X-Signature"] == expected # cafe24's shared-hosting WAF blocks the default python-requests/x.y User-Agent with an # HTML 403 (curl passes) — a custom UA is required or every event silently 403s. assert captured["headers"]["User-Agent"] == "move-detect-edge/1.0" # The .htaccess http->https 302 must NOT be auto-followed as a GET (405/404); a redirect # is a misconfiguration to surface, not to silently downgrade the POST. assert captured["kw"].get("allow_redirects") is False def test_send_classifies_5xx_as_transient(): # 5xx / 429 are "retry", not a permanent verdict — the offline queue must keep retrying, not # count these toward the poison drop cap. (A bare bool could not express this distinction.) client = PostClient("https://x", "pi-01", "s", lambda: "n", lambda: 1.0, http_post=lambda *a, **k: _FakeResp(500)) assert client.send(_event(), []) is SendResult.TRANSIENT for code in (408, 425, 429): # try-again 4xx codes are transient, not permanent rejections c = PostClient("https://x", "pi-01", "s", lambda: "n", lambda: 1.0, http_post=lambda *a, _c=code, **k: _FakeResp(_c)) assert c.send(_event(), []) is SendResult.TRANSIENT def test_send_classifies_4xx_as_rejected(): # A definite 4xx (not 429) means the server will never accept this event — the queue counts it # toward the poison drop cap so it can eventually be discarded and unblock the queue. client = PostClient("https://x", "pi-01", "s", lambda: "n", lambda: 1.0, http_post=lambda *a, **k: _FakeResp(400)) assert client.send(_event(), []) is SendResult.REJECTED def test_send_treats_transport_error_as_transient(): def boom(*a, **k): raise ConnectionError("network down") client = PostClient("https://x", "pi-01", "s", lambda: "n", lambda: 1.0, http_post=boom) assert client.send(_event(), []) is SendResult.TRANSIENT