First product

This commit is contained in:
Justin Visser 2026-02-17 14:51:25 +01:00
parent 778da9e2fc
commit 4433d7d2c4
157 changed files with 23975 additions and 0 deletions

204
tests/unit/test_auth.py Normal file
View file

@ -0,0 +1,204 @@
import re
from collections.abc import AsyncIterator
from types import SimpleNamespace
from unittest.mock import AsyncMock
from fastapi.testclient import TestClient
import pytest
from app.auth.deps import get_auth_service, get_login_rate_limiter
from app.auth.rate_limit import SlidingWindowRateLimiter
from app.db.session import get_db_session
from app.main import app
CSRF_TOKEN_PATTERN = re.compile(r'name="csrf_token" value="([^"]+)"')
class StubAuthService:
def __init__(self, *, user: object | None) -> None:
self._user = user
async def authenticate_user(self, _db_session, *, email: str, password: str):
if self._user is None:
return None
if email.strip().lower() != str(self._user.email):
return None
if password != "correct-password":
return None
return self._user
def _extract_csrf_token(html: str) -> str:
match = CSRF_TOKEN_PATTERN.search(html)
assert match is not None
return match.group(1)
async def _override_db_session() -> AsyncIterator[object]:
yield object()
@pytest.fixture(autouse=True)
def clear_dependency_overrides() -> AsyncIterator[None]:
app.dependency_overrides.clear()
yield
app.dependency_overrides.clear()
def test_login_requires_csrf_token() -> None:
client = TestClient(app)
response = client.post(
"/login",
data={"email": "user@example.com", "password": "correct-password"},
follow_redirects=False,
)
assert response.status_code == 403
assert response.text == "CSRF token missing."
def test_successful_login_creates_session_and_allows_dashboard(monkeypatch) -> None:
limiter = SlidingWindowRateLimiter(max_attempts=5, window_seconds=60)
app.dependency_overrides[get_db_session] = _override_db_session
app.dependency_overrides[get_auth_service] = lambda: StubAuthService(
user=SimpleNamespace(id=1, email="user@example.com", is_admin=False)
)
app.dependency_overrides[get_login_rate_limiter] = lambda: limiter
client = TestClient(app)
monkeypatch.setattr(
"app.web.common.get_authenticated_user",
AsyncMock(
return_value=SimpleNamespace(
id=1,
email="user@example.com",
is_admin=False,
)
),
)
monkeypatch.setattr(
"app.web.routers.dashboard.publication_service.list_new_for_latest_run_for_user",
AsyncMock(return_value=[]),
)
monkeypatch.setattr(
"app.web.routers.dashboard.run_service.list_recent_runs_for_user",
AsyncMock(return_value=[]),
)
monkeypatch.setattr(
"app.web.routers.dashboard.run_service.queue_status_counts_for_user",
AsyncMock(return_value={"queued": 0, "retrying": 0, "dropped": 0}),
)
monkeypatch.setattr(
"app.web.routers.dashboard.user_settings_service.get_or_create_settings",
AsyncMock(return_value=SimpleNamespace(request_delay_seconds=0)),
)
login_page = client.get("/login")
csrf_token = _extract_csrf_token(login_page.text)
login_response = client.post(
"/login",
data={
"email": "user@example.com",
"password": "correct-password",
"csrf_token": csrf_token,
},
follow_redirects=False,
)
assert login_response.status_code == 303
assert login_response.headers["location"] == "/"
dashboard_response = client.get("/")
assert dashboard_response.status_code == 200
assert 'data-test="home-hero"' in dashboard_response.text
assert 'data-test="session-user"' in dashboard_response.text
assert "user@example.com" in dashboard_response.text
def test_login_rate_limiting_returns_429_after_threshold() -> None:
limiter = SlidingWindowRateLimiter(max_attempts=2, window_seconds=60)
app.dependency_overrides[get_db_session] = _override_db_session
app.dependency_overrides[get_auth_service] = lambda: StubAuthService(user=None)
app.dependency_overrides[get_login_rate_limiter] = lambda: limiter
client = TestClient(app)
login_page = client.get("/login")
csrf_token = _extract_csrf_token(login_page.text)
payload = {
"email": "user@example.com",
"password": "wrong-password",
"csrf_token": csrf_token,
}
first = client.post("/login", data=payload, follow_redirects=False)
second = client.post("/login", data=payload, follow_redirects=False)
third = client.post("/login", data=payload, follow_redirects=False)
assert first.status_code == 401
assert second.status_code == 401
assert third.status_code == 429
assert third.headers["Retry-After"] == "60"
def test_logout_requires_csrf_token_and_clears_session(monkeypatch) -> None:
limiter = SlidingWindowRateLimiter(max_attempts=5, window_seconds=60)
app.dependency_overrides[get_db_session] = _override_db_session
app.dependency_overrides[get_auth_service] = lambda: StubAuthService(
user=SimpleNamespace(id=1, email="user@example.com", is_admin=False)
)
app.dependency_overrides[get_login_rate_limiter] = lambda: limiter
client = TestClient(app)
monkeypatch.setattr(
"app.web.common.get_authenticated_user",
AsyncMock(
side_effect=[
SimpleNamespace(id=1, email="user@example.com", is_admin=False),
None,
]
),
)
monkeypatch.setattr(
"app.web.routers.dashboard.publication_service.list_new_for_latest_run_for_user",
AsyncMock(return_value=[]),
)
monkeypatch.setattr(
"app.web.routers.dashboard.run_service.list_recent_runs_for_user",
AsyncMock(return_value=[]),
)
monkeypatch.setattr(
"app.web.routers.dashboard.run_service.queue_status_counts_for_user",
AsyncMock(return_value={"queued": 0, "retrying": 0, "dropped": 0}),
)
monkeypatch.setattr(
"app.web.routers.dashboard.user_settings_service.get_or_create_settings",
AsyncMock(return_value=SimpleNamespace(request_delay_seconds=0)),
)
login_page = client.get("/login")
csrf_token = _extract_csrf_token(login_page.text)
client.post(
"/login",
data={
"email": "user@example.com",
"password": "correct-password",
"csrf_token": csrf_token,
},
follow_redirects=False,
)
failed_logout = client.post("/logout", data={}, follow_redirects=False)
assert failed_logout.status_code == 403
assert failed_logout.text == "CSRF token invalid."
dashboard_page = client.get("/")
logout_token = _extract_csrf_token(dashboard_page.text)
successful_logout = client.post(
"/logout",
data={"csrf_token": logout_token},
follow_redirects=False,
)
assert successful_logout.status_code == 303
assert successful_logout.headers["location"] == "/login"
assert client.get("/", follow_redirects=False).headers["location"] == "/login"