loreal-utilisation-dept/backend/tests/test_auth.py
DJP 04edbfdd2c Initial commit: dockerised FastAPI backend + React/Vite frontend rewrite
Replaces a static SPA that shipped an Airtable PAT in the JS bundle.
The new architecture holds all secrets server-side, fronts the app
behind Apache on optical-dev with the shared-vhost split-build pattern,
and is designed for a later Azure AD/MSAL swap-in.

- backend/   FastAPI + uvicorn, local auth (Azure AD stub), Airtable
             proxy with TTL cache, Zoho .xlsx/.csv parser, merge
             service for utilisation summaries. 28 pytest tests.
- frontend/  React + Vite + TS + Tailwind + Recharts SPA. Login entry
             chunk 12.83 KB gzipped; Recharts lazy-loaded. No tokens
             or Airtable URLs in the built bundle.
- deploy/    Idempotent deploy.sh (port auto-pick 8200-8299,
             .env-persisted) + split-build Apache include template.
- docker-compose.yml pins name: utilisation-dept and binds 127.0.0.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:37:04 -04:00

88 lines
2.5 KiB
Python

"""Auth + session tests."""
from __future__ import annotations
import os
import pytest
from fastapi.testclient import TestClient
def _make_client():
# Ensure local mode + no bypass for these tests.
os.environ["AUTH_MODE"] = "local"
os.environ["DEV_AUTH_BYPASS"] = "false"
from app.config import get_settings
get_settings.cache_clear()
# Re-import main so middleware picks up settings.
import importlib
import app.main as main_mod
importlib.reload(main_mod)
# base_url must be https so the Secure session cookie is sent back.
return TestClient(main_mod.app, base_url="https://testserver")
@pytest.fixture
def client():
yield _make_client()
def test_login_happy_path(client):
r = client.post("/api/auth/login", json={"username": "admin", "password": "admin"})
assert r.status_code == 200, r.text
body = r.json()
assert body["ok"] is True
assert body["username"] == "admin"
# Session cookie present.
assert "ud_session" in r.cookies
def test_login_wrong_password(client):
r = client.post("/api/auth/login", json={"username": "admin", "password": "nope"})
assert r.status_code == 401
assert r.json()["detail"] == "Invalid credentials"
def test_me_requires_session(client):
r = client.get("/api/auth/me")
assert r.status_code == 401
def test_me_after_login(client):
r = client.post("/api/auth/login", json={"username": "admin", "password": "admin"})
assert r.status_code == 200
r2 = client.get("/api/auth/me")
assert r2.status_code == 200
assert r2.json()["username"] == "admin"
def test_logout(client):
client.post("/api/auth/login", json={"username": "admin", "password": "admin"})
r = client.post("/api/auth/logout")
assert r.status_code == 204
def test_bypass_mode():
os.environ["DEV_AUTH_BYPASS"] = "true"
from app.config import get_settings
get_settings.cache_clear()
import importlib
import app.main as main_mod
importlib.reload(main_mod)
c = TestClient(main_mod.app, base_url="https://testserver")
r = c.get("/api/auth/me")
assert r.status_code == 200
assert r.json()["mode"] == "bypass"
# Restore for any subsequent tests.
os.environ["DEV_AUTH_BYPASS"] = "false"
get_settings.cache_clear()
importlib.reload(main_mod)
def test_login_rate_limit(client):
# 5/minute → 6th attempt should 429.
last = None
for _ in range(6):
last = client.post("/api/auth/login", json={"username": "admin", "password": "wrong"})
assert last is not None
assert last.status_code == 429