"""Tests for the /me/profile/refetch-groups POST endpoint. The GET /me/debug page no longer exists — its content was folded into /me/profile as a collapsible "Session & troubleshooting" section. The refetch-groups POST: - Must be 404 (not 403) when ``AGNES_DEBUG_AUTH`` is unset / falsy. - Must return the documented diff shape and perform zero database writes. """ from __future__ import annotations import tempfile import uuid import pytest @pytest.fixture def fresh_db(monkeypatch): """Per-test DATA_DIR + JWT secret so the system DB is fresh.""" with tempfile.TemporaryDirectory() as tmp: monkeypatch.setenv("DATA_DIR", tmp) monkeypatch.setenv("TESTING", "1") monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!") yield tmp def _make_user_and_session(conn, email: str = "u@example.com"): """Create a non-admin user, return (user_id, session_jwt).""" from src.repositories.users import UserRepository from app.auth.jwt import create_access_token uid = str(uuid.uuid4()) UserRepository(conn).create( id=uid, email=email, name=email.split("@")[0] ) token = create_access_token(user_id=uid, email=email) return uid, token def _client(): from fastapi.testclient import TestClient from app.main import app return TestClient(app) # --------------------------------------------------------------------------- # Refetch endpoint — dry-run, zero DB writes # --------------------------------------------------------------------------- class TestRefetchDryRun: def test_404_when_flag_off(self, fresh_db, monkeypatch): monkeypatch.delenv("AGNES_DEBUG_AUTH", raising=False) from src.db import get_system_db, close_system_db conn = get_system_db() try: _, sess = _make_user_and_session(conn) finally: conn.close() close_system_db() c = _client() resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess}) assert resp.status_code == 404 def test_returns_diff_shape_and_does_not_write(self, fresh_db, monkeypatch): """Mocked Google response, refetch must return the documented shape AND not change any user_group_members rows.""" monkeypatch.setenv("AGNES_DEBUG_AUTH", "true") # Mock fetch to return a deterministic list (no real Google call). monkeypatch.setenv( "GOOGLE_ADMIN_SDK_MOCK_GROUPS", "grp_admin@example.com,grp_finance@example.com", ) from src.db import get_system_db, close_system_db conn = get_system_db() try: uid, sess = _make_user_and_session(conn, email="m@example.com") before_rows = conn.execute( "SELECT user_id, group_id, source FROM user_group_members " "WHERE user_id = ?", [uid], ).fetchall() finally: conn.close() close_system_db() c = _client() resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess}) assert resp.status_code == 200, resp.text data = resp.json() # Documented shape — keys present, types right. for key in ( "soft_failed", "prefix", "fetched", "fetched_relevant", "current_names", "current_external_ids", "would_add", "would_remove", "applied", ): assert key in data, f"missing key {key!r}" assert data["applied"] is False assert data["soft_failed"] is False assert isinstance(data["fetched"], list) assert isinstance(data["would_add"], list) # Zero DB writes — snapshot before/after must match exactly. conn = get_system_db() try: after_rows = conn.execute( "SELECT user_id, group_id, source FROM user_group_members " "WHERE user_id = ?", [uid], ).fetchall() finally: conn.close() close_system_db() assert before_rows == after_rows def test_soft_fail_marker_when_mock_unset_and_real_path_unconfigured( self, fresh_db, monkeypatch ): """Without the mock env and without GOOGLE_ADMIN_SDK_SUBJECT, the real path returns soft-fail; the endpoint reports it as such.""" monkeypatch.setenv("AGNES_DEBUG_AUTH", "true") monkeypatch.delenv("GOOGLE_ADMIN_SDK_MOCK_GROUPS", raising=False) monkeypatch.delenv("GOOGLE_ADMIN_SDK_SUBJECT", raising=False) from src.db import get_system_db, close_system_db conn = get_system_db() try: _, sess = _make_user_and_session(conn, email="sf@example.com") finally: conn.close() close_system_db() c = _client() resp = c.post("/me/profile/refetch-groups", cookies={"access_token": sess}) assert resp.status_code == 200, resp.text data = resp.json() # On the keyless-DWD branch, fetch_user_groups returns [] on missing # subject (legacy fail-soft as empty list); on the prefix-mapping # branch it returns None. Tolerate either — endpoint reports # soft_failed=True when None, False+empty list when []. if data["soft_failed"]: assert data["fetched"] == [] else: # Real path returned [] — also a valid shape; assert no writes # happened by virtue of applied=False + DB snapshot below. assert data["fetched"] == [] assert data["applied"] is False