Files
Meal-Planner/backend/tests/test_auth.py
T
admin 2695ba0232
CI / backend (pytest + alembic) (push) Canceled after 0s
CI / frontend (build) (push) Canceled after 0s
feat(meals): enforce three lower-calorie dinners with media
2026-09-01 10:11:36 -07:00

132 lines
4.4 KiB
Python

"""
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
from starlette.requests import Request
# 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")
os.environ.setdefault("SESSION_COOKIE_SECURE", "false")
@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")
monkeypatch.setenv("SESSION_COOKIE_SECURE", "false")
# 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
# ---------------------------------------------------------------------------
def test_admin_principal_defaults_when_email_is_unset(monkeypatch):
from app import security
monkeypatch.setattr(security.settings, "ADMIN_TOKEN", "test-admin-token")
monkeypatch.setattr(security.settings, "ADMIN_EMAIL", None)
request = Request(
{
"type": "http",
"headers": [(b"authorization", b"Bearer test-admin-token")],
}
)
assert security.require_admin(request) == "admin"
@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_required_for_reads(client):
"""GET /api/profile requires a session for non-LAN exposure."""
r = client.get("/api/profile")
assert r.status_code == 401, 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