"""Admin API: auth, overview/quality/costs shapes, sensor actions, company creation, queue, reviews, event corrections, cache.""" from __future__ import annotations import pytest import test_api_support as support client = support.client fixture_data = support.fixture_data ADMIN = support.ADMIN from companyatlas.api.common import cache from companyatlas.db import execute, fetch_one, fetch_val, transaction pytestmark = pytest.mark.asyncio(loop_scope="session") V = "/api/v1/admin" async def test_admin_requires_token(client): # type: ignore[no-untyped-def] for path in ("/overview", "/connectors", "/sensors", "/companies", "/failures", "/queue", "/llm", "/reviews", "/quality", "/costs"): r = await client.get(f"{V}{path}") assert r.status_code == 401 and r.json() == {"detail": "admin token required"}, path assert (await client.get(f"{V}/overview", headers={"X-CA-Admin-Token": "wrong"})).status_code == 401 assert (await client.post(f"{V}/cache/clear")).status_code == 401 async def test_overview_connectors_quality_costs(client, fixture_data): # type: ignore[no-untyped-def] r = await client.get(f"{V}/overview", headers=ADMIN) assert r.status_code == 200 and r.headers["cache-control"] == "no-store" and r.headers["x-ratelimit-tier"] == "admin" body = r.json() for key in ("companies_by_status", "sensors_by_status", "sensors_by_tier", "queue", "llm", "failures_24h_by_class", "fetch_rate_1h", "change_rate_1h", "meaningful_rate_1h", "storage", "workers", "cost_today"): assert key in body, key assert body["queue"]["dead"] >= 1 and body["failures_24h_by_class"].get("HTTP_5XX", 0) >= 1 and set(body["cost_today"]) >= {"fetch", "browser", "llm"} conns = (await client.get(f"{V}/connectors", headers=ADMIN)).json()["items"] mine = next(c for c in conns if c["id"] == fixture_data["connector"]) assert mine["sensors_active"] == 3 and {"success_rate_24h", "avg_latency_ms", "change_rate_24h", "errors_24h", "last_run_at"} <= set(mine) q = (await client.get(f"{V}/quality", headers=ADMIN)).json() for key in ("coverage", "freshness", "duplicate_rate", "event_confidence_avg", "unknown_surfaces", "failed_sensors", "calibration"): assert key in q assert set(q["calibration"]) == {"correct", "duplicate", "noise", "misclassified"} c = (await client.get(f"{V}/costs?days=7", headers=ADMIN)).json() assert {"items", "per_1000_companies", "per_million_observations", "per_meaningful_event"} <= set(c) async def test_sensors_list_filters_and_actions(client, fixture_data): # type: ignore[no-untyped-def] sid = fixture_data["sensor_beta"] r = await client.get(f"{V}/sensors", headers=ADMIN, params={"company": fixture_data["beta_slug"]}) body = r.json() assert r.status_code == 200 and body["total"] == 1 and body["items"][0]["company"]["slug"] == fixture_data["beta_slug"] assert (await client.get(f"{V}/sensors", headers=ADMIN, params={"filter": "healthy", "connector": fixture_data["connector"]})).json()["total"] == 3 assert (await client.get(f"{V}/sensors", headers=ADMIN, params={"filter": "high_activity", "connector": fixture_data["connector"]})).json()["total"] == 3 assert (await client.get(f"{V}/sensors", headers=ADMIN, params={"filter": "nope"})).status_code == 422 r = await client.post(f"{V}/sensors/{sid}/pause", headers=ADMIN) assert r.status_code == 200 and r.json()["sensor"]["status"] == "paused" assert (await client.get(f"{V}/sensors", headers=ADMIN, params={"status": "paused", "domain": f"{fixture_data['beta_slug']}.example"})).json()["total"] == 1 r = await client.post(f"{V}/sensors/{sid}/resume", headers=ADMIN) assert r.json()["sensor"]["status"] == "active" and r.json()["sensor"]["consecutive_failures"] == 0 r = await client.post(f"{V}/sensors/{sid}/set_interval", headers=ADMIN, json={"interval_s": 3600}) assert r.json()["interval_s"] == 3600 and r.json()["sensor"]["current_interval_s"] == 3600 and r.json()["sensor"]["tier"] == "B" assert (await client.post(f"{V}/sensors/{sid}/set_interval", headers=ADMIN, json={})).status_code == 422 assert (await client.post(f"{V}/sensors/{sid}/set_connector", headers=ADMIN, json={"connector_id": "nope-v9"})).status_code == 422 assert (await client.post(f"{V}/sensors/{sid}/set_connector", headers=ADMIN, json={"connector_id": fixture_data["connector"]})).status_code == 200 r = await client.post(f"{V}/sensors/{sid}/run_now", headers=ADMIN) assert r.status_code == 200 r = await client.post(f"{V}/sensors/{sid}/rediscover", headers=ADMIN, json={"reason": "test"}) assert r.status_code == 200 and r.json()["queued"]["key"].startswith(f"discover:{fixture_data['beta']}:") async with transaction() as conn: job = await fetch_one(conn, "select kind, status, payload from queue_jobs where id = :id", id=r.json()["queued"]["id"]) assert job["kind"] == "discover" and job["status"] == "pending" r = await client.post(f"{V}/sensors/{sid}/retire", headers=ADMIN) assert r.json()["sensor"]["status"] == "retired" and r.json()["sensor"]["retired_at"] assert (await client.post(f"{V}/sensors/{sid}/resume", headers=ADMIN)).json()["sensor"]["status"] == "active" assert (await client.post(f"{V}/sensors/{sid}/explode", headers=ADMIN)).status_code == 404 assert (await client.post(f"{V}/sensors/sen_nope/pause", headers=ADMIN)).status_code == 404 async def test_companies_list_create_rediscover(client, fixture_data): # type: ignore[no-untyped-def] r = await client.get(f"{V}/companies", headers=ADMIN, params={"onboarding_status": "active", "country": "ZZ"}) assert r.status_code == 200 and r.json()["total"] == 2 and "onboarding_error" in r.json()["items"][0] suffix = fixture_data["suffix"] r = await client.post(f"{V}/companies", headers=ADMIN, json={"website": f"https://www.ztest-created-{suffix}.example/home?utm_source=x", "display_name": f"Ztest Created {suffix}", "country": "ZZ", "industries": [fixture_data["industry"]]}) assert r.status_code == 201, r.text body = r.json() card = body["company"] assert card["canonical_domain"] == f"ztest-created-{suffix}.example" and card["website"] == f"https://www.ztest-created-{suffix}.example/home" assert card["slug"] == f"ztest-created-{suffix}" and card["onboarding_status"] == "pending" and body["queued"]["kind"] == "discover" async with transaction() as conn: assert await fetch_val(conn, "select status from queue_jobs where key = :k", k=f"discover:{card['id']}") == "pending" dup = await client.post(f"{V}/companies", headers=ADMIN, json={"website": f"http://ztest-created-{suffix}.example"}) assert dup.status_code == 409 assert (await client.post(f"{V}/companies", headers=ADMIN, json={"website": "ftp://ztest.example"})).status_code == 422 assert (await client.post(f"{V}/companies", headers=ADMIN, json={"website": "https://localhost"})).status_code == 422 assert (await client.post(f"{V}/companies", headers=ADMIN, json={"website": f"https://ztest-created2-{suffix}.example", "country": "QQ"})).status_code == 422 assert (await client.post(f"{V}/companies", headers=ADMIN, json={"website": f"https://ztest-created2-{suffix}.example", "industries": ["nope-ind"]})).status_code == 422 r = await client.post(f"{V}/companies/{card['slug']}/rediscover", headers=ADMIN) assert r.status_code == 200 and r.json()["queued"]["key"].startswith(f"discover:{card['id']}:") assert (await client.post(f"{V}/companies/nope/rediscover", headers=ADMIN)).status_code == 404 async with transaction() as conn: # remove the created company so the ZZ counts used by the other modules stay stable await execute(conn, "delete from queue_jobs where key like :k", k=f"discover:{card['id']}%") await execute(conn, "delete from companies where id = :id", id=card["id"]) cache.clear() async def test_failures_queue_llm_reviews(client, fixture_data): # type: ignore[no-untyped-def] r = await client.get(f"{V}/failures", headers=ADMIN, params={"class": "http_5xx", "company": fixture_data["beta_slug"]}) assert r.status_code == 200 and r.json()["total"] == 1 and r.json()["by_class"] == {"HTTP_5XX": 1} and "HTTP_5XX" in r.json()["classes"] q = (await client.get(f"{V}/queue", headers=ADMIN, params={"status": "dead"})).json() assert any(i["id"] == fixture_data["queue_dead"] for i in q["items"]) and any(c["status"] == "dead" for c in q["counts"]) r = await client.post(f"{V}/queue/requeue-dead", headers=ADMIN, json={"kind": "run_sensor"}) assert r.status_code == 200 and r.json()["requeued"] >= 1 async with transaction() as conn: assert await fetch_val(conn, "select status from queue_jobs where id = :id", id=fixture_data["queue_dead"]) == "pending" llm = (await client.get(f"{V}/llm", headers=ADMIN)).json() assert {"items", "stats"} <= set(llm) and "budget" in llm["stats"] reviews = (await client.get(f"{V}/reviews", headers=ADMIN, params={"kind": "major_event"})).json() assert any(x["id"] == fixture_data["review"] for x in reviews["items"]) and reviews["open_by_kind"].get("major_event", 0) >= 1 r = await client.post(f"{V}/reviews/{fixture_data['review']}", headers=ADMIN, json={"resolution": "accepted", "label": "correct", "note": "looks right"}) assert r.status_code == 200 and r.json()["review"]["status"] == "accepted" and r.json()["review"]["payload"]["resolution"]["label"] == "correct" assert (await client.post(f"{V}/reviews/{fixture_data['review']}", headers=ADMIN, json={"resolution": "rejected"})).status_code == 409 assert (await client.post(f"{V}/reviews/rev_nope", headers=ADMIN, json={"resolution": "rejected"})).status_code == 404 assert (await client.get(f"{V}/quality", headers=ADMIN)).json()["calibration"]["correct"] >= 1 async def test_event_retract_restore_and_cache_clear(client, fixture_data): # type: ignore[no-untyped-def] eid = fixture_data["ev_product"] r = await client.post(f"{V}/events/{eid}/retract", headers=ADMIN, json={"reason": "synthetic correction"}) assert r.status_code == 200 and r.json()["event"]["status"] == "retracted" and r.json()["event"]["retracted_reason"] == "synthetic correction" assert (await client.get(f"/api/v1/events/{eid}")).json()["status"] == "retracted" assert all(e["id"] != eid for e in (await client.get("/api/v1/live?limit=50")).json()["items"]) assert (await client.post(f"{V}/events/{eid}/retract", headers=ADMIN, json={"reason": "x"})).status_code == 422 r = await client.post(f"{V}/events/{eid}/restore", headers=ADMIN) assert r.status_code == 200 and r.json()["event"]["status"] == "active" audit = r.json()["event"]["payload"]["_audit"] assert [a["action"] for a in audit] == ["retract", "restore"] and audit[0]["reason"] == "synthetic correction" assert (await client.post(f"{V}/events/evt_nope/restore", headers=ADMIN)).status_code == 404 r = await client.post(f"{V}/cache/clear", headers=ADMIN) assert r.status_code == 200 and r.json()["ok"] is True assert (await client.post(f"{V}/cache/clear?prefix=pulse", headers=ADMIN)).json()["cleared"] == "pulse"