"""API-level tests: auth gates, case lifecycle, suppression rules, send gating. Runs against an in-memory database with CX-Tools stubbed out, so it needs no credentials. Prometheus is only touched for cache warming, which is tolerant of being unreachable. """ import os import sys import tempfile sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) os.environ.update({ "CX_DATABASE_URL": f"sqlite:///{tempfile.mkdtemp()}/test.db", "CX_BOOTSTRAP_ADMIN_EMAIL": "admin@localhost", "CX_BOOTSTRAP_ADMIN_PASSWORD": "test-password", "CX_SECRET_KEY": "test-secret", "CX_FEATURE_SEND_ENABLED": "false", }) from triagelib import cxbridge # noqa: E402 cxbridge.bootstrap = lambda: (_ for _ in ()).throw(cxbridge.BridgeError("stubbed")) from fastapi.testclient import TestClient # noqa: E402 from app import auth as auth_mod # noqa: E402 from app.main import app # noqa: E402 FAILS = [] def expect(label, cond, got=""): print((" PASS " if cond else " FAIL ") + label + ("" if cond else f" <- {got}")) if not cond: FAILS.append(label) print("\nPASSWORDS AND SESSIONS") h = auth_mod.hash_password("hunter2") expect("correct password verifies", auth_mod.verify_password("hunter2", h)) expect("wrong password rejected", not auth_mod.verify_password("hunter3", h)) expect("hash is salted (two hashes differ)", auth_mod.hash_password("x") != auth_mod.hash_password("x")) token = auth_mod.issue_session(7) expect("session round-trips", auth_mod.read_session(token) == 7) expect("tampered session rejected", auth_mod.read_session(token[:-4] + "aaaa") is None) expect("garbage session rejected", auth_mod.read_session("not-a-token") is None) expect("oidc state verifies", auth_mod.oidc_state_valid(auth_mod.oidc_state())) expect("forged oidc state rejected", not auth_mod.oidc_state_valid("aaa.bbb")) with TestClient(app) as client: print("\nAUTH GATES") expect("health is public", client.get("/api/health").status_code == 200) expect("alerts need a session", client.get("/api/alerts").status_code == 401) expect("cases need a session", client.get("/api/cases").status_code == 401) expect("wrong password is 401", client.post( "/api/auth/login", json={"email": "admin@localhost", "password": "no"}).status_code == 401) login = client.post("/api/auth/login", json={"email": "admin@localhost", "password": "test-password"}) expect("login succeeds", login.status_code == 200, login.text[:120]) expect("bootstrap user is admin", login.json()["user"]["is_admin"]) expect("session works after login", client.get("/api/cases").status_code == 200) print("\nCONFIG EXPOSURE") cfg = client.get("/api/auth/me").json()["config"] expect("send disabled by default", cfg["send_enabled"] is False, cfg) expect("no secret leaks into public config", not any("token" in k.lower() or "secret" in k.lower() for k in cfg), list(cfg)) print("\nSUPPRESSION RULES") rule = {"name": "Modal ERROR churn", "reason": "known batch churn", "conditions": {"kind": ["error"], "organization": ["modal"]}} expect("admin can save a rule", client.post("/api/settings/rules", json=rule).status_code == 200) expect("rule is persisted", len(client.get("/api/settings").json()["rules"]) == 1) expect("a rule with no conditions is refused", client.post( "/api/settings/rules", json={"name": "catch all", "conditions": {}}).status_code == 400) expect("unknown condition fields are dropped", client.post( "/api/settings/rules", json={"name": "bogus", "conditions": {"nope": ["x"]}}).status_code == 400) print("\nSEND GATING") status = client.get("/api/actions/status").json() expect("send reported as disabled", status["send_enabled"] is False) expect("zendesk reported as not ready", status["zendesk_ready"] is False) blocked = client.post("/api/actions/zendesk", json={ "fingerprint": "does-not-exist", "to": "a@b.c", "subject": "s", "body": "b"}) expect("sending on an untracked alert is refused", blocked.status_code == 404, blocked.text[:120]) print("\nCASE LIFECYCLE") from app.db import SessionLocal from app.models import Case, CaseStatus from app.services import add_event, set_status db = SessionLocal() case = Case(fingerprint="test-fp", kind="error", title="Instance in ERROR state", subject="vm-1") db.add(case) db.commit() expect("new case starts open", case.is_open and case.status == CaseStatus.NEW) set_status(db, case, CaseStatus.ESCALATED_INFRA, None, "INFRA-1") expect("escalated is still open", case.is_open) expect("status change is recorded", any(e.action == "status_changed" for e in case.events)) set_status(db, case, CaseStatus.RESOLVED, None) expect("resolved closes the case", not case.is_open and case.closed_at is not None) add_event(db, case, None, "note", "manual note") db.commit() expect("history is append-only and ordered newest first", case.events[0].action in ("note", "status_changed"), [e.action for e in case.events]) payload = case.to_json(with_events=True) expect("serialises for the API", payload["status"] == "resolved" and len(payload["events"]) >= 3) db.close() expect("bad status is rejected", client.post( "/api/cases/test-fp/status", json={"status": "banana"}).status_code == 400) expect("unknown case is 404", client.get("/api/cases/nope").status_code == 404) print("\nLOGOUT") client.post("/api/auth/logout") expect("session is cleared", client.get("/api/cases").status_code == 401) print("\n" + ("ALL CHECKS PASSED" if not FAILS else f"{len(FAILS)} CHECK(S) FAILED: {FAILS}")) sys.exit(1 if FAILS else 0)