Files
LedgerLine/tests/conftest.py
T
Movses Aslanyan ccf1783779 Add Phase 0a black-box integration test suite
81 tests across 11 files plus a 5-module harness (load, pg, streams,
containers, payloads), covering all eleven Phase 0 goals and all 13 named
chaos-flag pathologies. Talks to the system only over HTTP, Postgres, Redis,
and the Docker CLI -- no imports of application code. RED-BASELINE.md records
the suite failing red against the empty repo before any implementation
existed; README.md documents the request-contract fields invented to fill
gaps in the spec, measurement methodology, and known-flaky tests.

This suite is a frozen contract from this point forward per CLAUDE.md.
2026-09-05 08:11:59 +03:30

150 lines
4.9 KiB
Python

"""Shared fixtures for the Ledgerline black-box integration suite.
Per phase-0a-test-suite-spec.md §8. Every environment value (ports, DSN, Redis
URL) comes from harness.config, itself env-driven with the Compose defaults
as fallbacks -- no hardcoded values in test bodies.
"""
import time
from dataclasses import dataclass
import httpx
import psycopg
import pytest
import pytest_asyncio
import redis as redis_lib
from harness.config import GATEWAY_URL, HTTP_SERVICES, POSTGRES_DSN, REDIS_URL, SERVICE_URLS
@pytest.fixture(scope="session")
def dsn():
return POSTGRES_DSN
@pytest.fixture(scope="session")
def redis_client():
client = redis_lib.from_url(REDIS_URL, decode_responses=True)
yield client
client.close()
@pytest.fixture(scope="session", autouse=True)
def stack_up():
"""Fail fast, with a clear message, if the Compose stack isn't up and ready.
Every test in this suite implicitly depends on this: none of them are
meaningful against a partially-started stack, and none of them should hang
waiting for services that were simply never started.
"""
deadline = time.monotonic() + 30
unhealthy = dict(SERVICE_URLS)
with httpx.Client(timeout=5.0) as client:
while unhealthy and time.monotonic() < deadline:
for name, url in list(unhealthy.items()):
try:
resp = client.get(f"{url}/healthz")
if resp.status_code == 200:
del unhealthy[name]
except httpx.HTTPError:
pass
if unhealthy:
time.sleep(1)
if unhealthy:
pytest.fail(
"Stack is not up: " + ", ".join(sorted(unhealthy)) +
" did not answer /healthz within 30s. Run `make up && make seed` first.",
pytrace=False,
)
@pytest.fixture(scope="session")
def seeded(dsn):
"""Fail fast if the seed manifest is missing or empty."""
try:
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("SELECT data FROM seed_manifest LIMIT 1")
row = cur.fetchone()
except Exception as exc:
pytest.fail(f"Seed manifest not readable ({exc}). Run `make seed` first.", pytrace=False)
if not row or not row[0] or not row[0].get("counts", {}).get("transactions"):
pytest.fail("Seed manifest is empty or has zero transactions. Run `make seed` first.", pytrace=False)
@pytest.fixture(scope="session")
def manifest(seeded, dsn):
"""The parsed seed manifest (db/seed_manifest.json via the seed_manifest table)."""
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("SELECT data FROM seed_manifest LIMIT 1")
(data,) = cur.fetchone()
return data
@pytest_asyncio.fixture
async def api():
"""httpx.AsyncClient against the gateway, 15s timeout."""
async with httpx.AsyncClient(base_url=GATEWAY_URL, timeout=15.0) as client:
yield client
@dataclass
class ChaosControl:
client: httpx.AsyncClient
async def get(self):
resp = await self.client.get("/api/admin/chaos")
resp.raise_for_status()
return resp.json()
async def set_flag(self, flag, enabled):
return await self.client.post("/api/admin/chaos", json={"flag": flag, "enabled": enabled})
async def set_profile(self, profile):
resp = await self.client.post("/api/admin/chaos/profile", json={"profile": profile})
resp.raise_for_status()
return resp.json()
@pytest_asyncio.fixture
async def chaos(api):
"""chaos.set_profile(name), chaos.set_flag(name, bool), chaos.get().
Teardown always resets the profile to `clean`, even if the test failed --
design principle #6 in phase-0a-test-suite-spec.md §2: every test that
enables a chaos flag must restore state.
"""
control = ChaosControl(api)
try:
yield control
finally:
await control.set_profile("clean")
@pytest.fixture
def fresh_balances(dsn):
"""fresh_balances(account_id) snapshots a balance now and restores it on
teardown, so balance assertions don't drift across repeated runs."""
tracked = {}
def track(account_id):
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("SELECT balance_minor FROM accounts WHERE id = %s", (account_id,))
row = cur.fetchone()
if row is None:
raise ValueError(f"unknown account {account_id}")
tracked[account_id] = row[0]
return row[0]
yield track
if tracked:
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
for account_id, balance in tracked.items():
cur.execute(
"UPDATE accounts SET balance_minor = %s WHERE id = %s",
(balance, account_id),
)