"""Operator API endpoint unit tests (TASK-08-05) — mocked PgStore. Covers: 401 without cookie, 200 with valid cookie, suppressed cells have value=null, last_updated is max(updated_at), credential revoke works, no per-learner data in responses (R-DASH-02). """ from __future__ import annotations import datetime as _dt from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from starlette.middleware.sessions import SessionMiddleware from server.auth.models import Operator from server.auth.passwords import hash_password from server.auth.rate_limit import reset_login_rate_limit from server.auth.routes import router as auth_router from server.operator.cohort import router as cohort_router from server.operator.credentials import router as credentials_router from server.operator.failure_patterns import router as failure_router from server.operator.mastery import router as mastery_router @pytest.fixture(autouse=True) def _reset_limiter(): reset_login_rate_limit() yield reset_login_rate_limit() class _FakeRecord(dict): pass def _mock_pg_store(aggregates=None, credentials=None): store = MagicMock() # Operator lookup for current_operator dependency. store.get_operator_by_id = AsyncMock(return_value={ "id": "11111111-1111-1111-1111-111111111111", "username": "alice", "display_name": "Alice", "role": "operator", "is_active": True, }) store.update_last_login = AsyncMock() store.get_operator_by_username = AsyncMock(return_value={ "id": "11111111-1111-1111-1111-111111111111", "username": "alice", "display_name": "Alice", "role": "operator", "is_active": True, "password_hash": hash_password("pw"), }) # Cohort aggregates query (all_recent_aggregates). aggregates = aggregates or [] conn = MagicMock() conn.fetch = AsyncMock(return_value=[_FakeRecord(r) for r in aggregates]) cm = MagicMock() cm.__aenter__ = AsyncMock(return_value=conn) cm.__aexit__ = AsyncMock(return_value=None) store.pool = MagicMock() store.pool.acquire = MagicMock(return_value=cm) # Credentials. store.list_credentials = AsyncMock(return_value=credentials or []) store.get_credential = AsyncMock(return_value=credentials[0] if credentials else None) store.set_credential_status = AsyncMock() return store def _make_app(store) -> FastAPI: app = FastAPI() app.state.pg_store = store app.add_middleware(SessionMiddleware, secret_key="test-secret-1234567890abcdef") app.include_router(auth_router) app.include_router(cohort_router) app.include_router(mastery_router) app.include_router(failure_router) app.include_router(credentials_router) return app def _login(client) -> None: r = client.post("/api/operator/login", json={"username": "alice", "password": "pw"}) assert r.status_code == 200, r.text # ── 401 without cookie ───────────────────────────────────────────────────── def test_cohort_401_without_cookie(): app = _make_app(_mock_pg_store()) with TestClient(app) as client: r = client.get("/api/operator/cohort") assert r.status_code == 401 def test_mastery_401_without_cookie(): app = _make_app(_mock_pg_store()) with TestClient(app) as client: r = client.get("/api/operator/mastery") assert r.status_code == 401 def test_failure_patterns_401_without_cookie(): app = _make_app(_mock_pg_store()) with TestClient(app) as client: r = client.get("/api/operator/failure-patterns") assert r.status_code == 401 def test_credentials_401_without_cookie(): app = _make_app(_mock_pg_store()) with TestClient(app) as client: r = client.get("/api/operator/credentials") assert r.status_code == 401 def test_revoke_401_without_cookie(): app = _make_app(_mock_pg_store()) with TestClient(app) as client: r = client.post("/api/operator/credentials/abc/revoke") assert r.status_code == 401 # ── 200 with valid cookie ────────────────────────────────────────────────── def test_cohort_200_with_cookie(): now = _dt.datetime.now(_dt.timezone.utc) agg = [ {"path": "customer_service", "metric": "sessions_count", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 12.0, "cell_count": 12, "cell_suppressed": False, "updated_at": now}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/cohort") assert r.status_code == 200 body = r.json() assert any(v["path"] == "customer_service" for v in body["views"]) def test_mastery_200_with_cookie(): agg = [ {"path": "p", "metric": "gate_open_rate", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 0.5, "cell_count": 10, "cell_suppressed": False, "updated_at": _dt.datetime.now(_dt.timezone.utc)}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/mastery") assert r.status_code == 200 def test_failure_patterns_200_with_cookie(): agg = [ {"path": "p", "metric": "failure_mode:missed_apology", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 3.0, "cell_count": 10, "cell_suppressed": False, "updated_at": _dt.datetime.now(_dt.timezone.utc)}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/failure-patterns") assert r.status_code == 200 def test_credentials_200_with_cookie(): cred = { "id": "11111111-1111-1111-1111-111111111111", "learner_ref": "learner-1", "vc_type": "MasteryCredential", "status": "active", "issued_at": _dt.datetime.now(_dt.timezone.utc), "revoked_at": None, } app = _make_app(_mock_pg_store(credentials=[cred])) with TestClient(app) as client: _login(client) r = client.get("/api/operator/credentials") assert r.status_code == 200 body = r.json() assert len(body["credentials"]) == 1 # ── Suppressed cells have value=null ─────────────────────────────────────── def test_suppressed_cells_value_null(): agg = [ {"path": "p", "metric": "sessions_count", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": None, "cell_count": 5, "cell_suppressed": True, "updated_at": _dt.datetime.now(_dt.timezone.utc)}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/cohort") assert r.status_code == 200 cell = r.json()["views"][0]["metrics"][0] assert cell["cell_suppressed"] is True assert cell["value"] is None # ── last_updated is max(updated_at) ──────────────────────────────────────── def test_last_updated_is_max(): t1 = _dt.datetime(2026, 8, 1, 12, 0, tzinfo=_dt.timezone.utc) t2 = _dt.datetime(2026, 8, 3, 12, 0, tzinfo=_dt.timezone.utc) agg = [ {"path": "p", "metric": "sessions_count", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 1.0, "cell_count": 10, "cell_suppressed": False, "updated_at": t1}, {"path": "p", "metric": "active_learners_count", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 10.0, "cell_count": 10, "cell_suppressed": False, "updated_at": t2}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/cohort") assert r.status_code == 200 assert r.json()["last_updated"] is not None # ── Credential revoke ────────────────────────────────────────────────────── def test_credential_revoke_sets_status_revoked(): cred = { "id": "22222222-2222-2222-2222-222222222222", "learner_ref": "learner-1", "vc_type": "MasteryCredential", "status": "active", "issued_at": _dt.datetime.now(_dt.timezone.utc), "revoked_at": None, } store = _mock_pg_store(credentials=[cred]) app = _make_app(store) with TestClient(app) as client: _login(client) r = client.post("/api/operator/credentials/22222222-2222-2222-2222-222222222222/revoke") assert r.status_code == 200 assert r.json()["status"] == "revoked" store.set_credential_status.assert_awaited_once_with( "22222222-2222-2222-2222-222222222222", "revoked", ) def test_credential_revoke_404_unknown(): store = _mock_pg_store(credentials=None) store.get_credential = AsyncMock(return_value=None) app = _make_app(store) with TestClient(app) as client: _login(client) r = client.post("/api/operator/credentials/nonexistent/revoke") assert r.status_code == 404 # ── No per-learner data in cohort responses (R-DASH-02) ─────────────────── def test_no_per_learner_data_in_cohort_response(): agg = [ {"path": "p", "metric": "sessions_count", "window_start": _dt.date.today(), "window_end": _dt.date.today(), "value": 10.0, "cell_count": 10, "cell_suppressed": False, "updated_at": _dt.datetime.now(_dt.timezone.utc)}, ] app = _make_app(_mock_pg_store(aggregates=agg)) with TestClient(app) as client: _login(client) r = client.get("/api/operator/cohort") body_text = r.text # No per-learner refs in the response (only path + metric + aggregates). assert "learner-1" not in body_text assert "learner_ref" not in body_text # ── 503 when no Postgres ─────────────────────────────────────────────────── def test_cohort_503_no_postgres(): app = FastAPI() app.state.pg_store = None app.add_middleware(SessionMiddleware, secret_key="test-secret-1234567890abcdef") app.include_router(auth_router) app.include_router(cohort_router) with TestClient(app) as client: r = client.get("/api/operator/cohort") assert r.status_code == 503