Files
LedgerLine/tests/test_contract_health.py
T

119 lines
4.8 KiB
Python
Raw Normal View History

"""G2 -- the system is genuinely multi-service, not a collapsed monolith."""
import asyncio
import time
import httpx
import pytest
from harness import containers
from harness.config import HTTP_SERVICES, SERVICE_URLS
from harness.payloads import tx_payload
pytestmark = pytest.mark.contract
@pytest.mark.parametrize("service", HTTP_SERVICES)
async def test_service_healthz_reports_own_name(service):
"""G2: every HTTP service answers /healthz on its own port with its own name."""
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{SERVICE_URLS[service]}/healthz")
assert resp.status_code == 200
assert resp.json().get("service") == service
@pytest.mark.parametrize("service", HTTP_SERVICES)
async def test_service_readyz_ok(service):
"""G2: /readyz returns 200 for all six services when dependencies are healthy."""
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{SERVICE_URLS[service]}/readyz")
assert resp.status_code == 200
async def test_health_all_reports_all_services_ok(api):
"""G2: GET /api/health/all reports all six services as ok."""
resp = await api.get("/api/health/all")
assert resp.status_code == 200
services = resp.json()["services"]
assert set(services) == set(HTTP_SERVICES)
assert all(v == "ok" for v in services.values()), services
async def test_stopping_sanctions_breaks_transactions(api, manifest):
"""G2: stopping `sanctions` must make it report unhealthy within 15s and
transactions must fail with 502/504, not 200 -- the test that catches a
collapsed monolith (if transactions still succeed, sanctions isn't really
in the path)."""
containers.stop("sanctions")
try:
deadline = time.monotonic() + 15
reported_unhealthy = False
while time.monotonic() < deadline:
resp = await api.get("/api/health/all")
if resp.status_code == 200 and resp.json()["services"].get("sanctions") != "ok":
reported_unhealthy = True
break
await asyncio.sleep(1)
assert reported_unhealthy, "sanctions was not reported unhealthy within 15s of being stopped"
resp = await api.post(
"/api/transactions", json=tx_payload(manifest["clean_account_id"])
)
assert resp.status_code in (502, 504), (
f"expected 502/504 with sanctions stopped, got {resp.status_code}: {resp.text}"
)
finally:
containers.start("sanctions")
containers.wait_ready("sanctions", timeout=30)
# ---------------------------------------------------------------------------
# G9 -- failure is contained: timeouts fire, the gateway doesn't wedge.
# ---------------------------------------------------------------------------
async def test_sanctions_errors_timeout_never_exceeds_10s(api, chaos, manifest, fresh_balances):
"""G9: with sanctions_errors on and retry_storm off, no request takes
longer than 10s -- the 3s downstream timeout must actually fire."""
account_id = manifest["clean_account_id"]
fresh_balances(account_id)
await chaos.set_profile("clean")
await chaos.set_flag("sanctions_errors", True)
for _ in range(20):
t0 = time.monotonic()
resp = await api.post("/api/transactions", json=tx_payload(account_id, amount_minor=100))
elapsed = time.monotonic() - t0
assert elapsed <= 10, f"request took {elapsed:.1f}s, expected <= 10s"
assert resp.status_code in (201, 502, 504), resp.status_code
async def test_gateway_healthz_stays_fast_while_sanctions_is_down(api):
"""G9: with sanctions stopped entirely, the gateway still answers
/healthz in under 1s throughout."""
containers.stop("sanctions")
try:
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
t0 = time.monotonic()
resp = await api.get("/healthz")
elapsed = time.monotonic() - t0
assert resp.status_code == 200
assert elapsed < 1, f"/healthz took {elapsed:.2f}s while sanctions was down"
await asyncio.sleep(0.5)
finally:
containers.start("sanctions")
containers.wait_ready("sanctions", timeout=30)
async def test_no_200_response_ever_has_a_null_transaction_id(api, chaos, manifest, fresh_balances):
"""G9: no request ever returns a 200/201 with a null transaction_id, even
under combined chaos (nightmare profile)."""
account_id = manifest["clean_account_id"]
fresh_balances(account_id)
await chaos.set_profile("nightmare")
for _ in range(20):
resp = await api.post("/api/transactions", json=tx_payload(account_id, amount_minor=100))
if resp.status_code in (200, 201):
body = resp.json()
assert body.get("transaction_id") is not None, body