""" Auth gate tests for R1-B+D. Covers: - /api/admin/* requires bearer token (401 without, not 401 with valid). - Mutating routes on family routers require a session cookie. - /api/auth/login + /api/auth/logout round-trip with the shared password. """ from __future__ import annotations import os import pytest # Configure auth secrets BEFORE app import. conftest.py runs first and sets # DATABASE_URL; we layer auth env on top here. os.environ.setdefault("ADMIN_TOKEN", "test-admin-token") os.environ.setdefault("SESSION_PASSWORD", "test-family-password") os.environ.setdefault("SECRET_KEY", "test-secret-key-do-not-use-in-prod") @pytest.fixture(autouse=True) def _reload_settings(monkeypatch): """Force ``settings`` to re-read env (test isolation).""" monkeypatch.setenv("ADMIN_TOKEN", "test-admin-token") monkeypatch.setenv("SESSION_PASSWORD", "test-family-password") monkeypatch.setenv("SECRET_KEY", "test-secret-key-do-not-use-in-prod") # Re-instantiate the singleton so dependents pick up env. from app import config as app_config app_config.settings = app_config.Settings() yield # --------------------------------------------------------------------------- # Admin bearer token # --------------------------------------------------------------------------- @pytest.mark.requires_postgres def test_admin_requires_token(client): """No bearer → 401. Valid bearer → not 401 (handler runs).""" r = client.post("/api/admin/scrape") assert r.status_code == 401, r.text r = client.post( "/api/admin/scrape", headers={"Authorization": "Bearer test-admin-token"}, ) # Handler may 200/202/500 (Playwright not installed in CI), but NOT 401. assert r.status_code != 401, r.text @pytest.mark.requires_postgres def test_admin_logs_requires_token(client): r = client.get("/api/admin/logs") assert r.status_code == 401 r = client.get( "/api/admin/logs", headers={"Authorization": "Bearer test-admin-token"} ) assert r.status_code == 200 # --------------------------------------------------------------------------- # Session-gated mutations # --------------------------------------------------------------------------- @pytest.mark.requires_postgres def test_session_required_for_mutation(client): """POST /api/profile/members without cookie → 401.""" r = client.post( "/api/profile/members", json={"name": "x", "email": "x@y.z", "role": "voter"}, ) assert r.status_code == 401, r.text @pytest.mark.requires_postgres def test_session_open_for_reads(client): """GET /api/profile is NOT auth-gated (reads stay open).""" r = client.get("/api/profile") # Either 200 (profile exists) or 404 (no profile yet) — never 401. assert r.status_code in (200, 404), r.text # --------------------------------------------------------------------------- # Login / logout # --------------------------------------------------------------------------- @pytest.mark.requires_postgres def test_session_login_logout(client): # Wrong password r = client.post("/api/auth/login", json={"password": "nope"}) assert r.status_code == 401 # Right password sets the cookie r = client.post( "/api/auth/login", json={"password": "test-family-password"} ) assert r.status_code == 204 assert "mp_session" in r.cookies, r.headers # With cookie, mutation succeeds (or fails for non-auth reasons) cookie_value = r.cookies.get("mp_session") client.cookies.set("mp_session", cookie_value) r2 = client.post( "/api/profile/members", json={"name": "x", "email": "test@example.com", "role": "voter"}, ) # 401 means the cookie was rejected — that's the bug we're guarding. assert r2.status_code != 401, r2.text # Logout clears the cookie r3 = client.post("/api/auth/logout") assert r3.status_code == 204