Public Access
- backend: settings SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH, require_session uses secrets.compare_digest and respects trusted-network opt-in, main.py adds require_family_session middleware gating all /api/ routes except auth/admin/email-vote-token paths - docker-compose: pass SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH through to backend + scheduler (fixes env-file changes not reaching runtime) - frontend: Ingress path-prefix support (APP_BASE_PATH, BrowserRouter basename, vite base './'), Login redirect honors APP_BASE_PATH - nginx: no-cache headers on root + /assets/ - docs: Home Assistant Ingress install/troubleshooting + plan file - tests: test_auth expects 401 on no-session GET Defaults: SESSION_COOKIE_SECURE=false, TRUSTED_NETWORK_AUTO_AUTH=true (HA is the auth boundary; MealPlanner must not be port-forwarded directly).
116 lines
3.9 KiB
Python
116 lines
3.9 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
|
|
|
|
|
|
# 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
|
|
# ---------------------------------------------------------------------------
|
|
@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
|