# ABOUTME: Integration tests for FastHTML application creation.
# ABOUTME: Tests app factory, middleware wiring, and route configuration.
import os
import pytest
def make_test_settings(
csrf_secret: str = "test-secret",
trusted_proxy_ips: str = "127.0.0.1",
auth_header_name: str = "X-Oidc-Username",
):
"""Create Settings for testing by setting env vars temporarily."""
from animaltrack.config import Settings
old_env = os.environ.copy()
try:
os.environ["CSRF_SECRET"] = csrf_secret
os.environ["TRUSTED_PROXY_IPS"] = trusted_proxy_ips
os.environ["AUTH_HEADER_NAME"] = auth_header_name
return Settings()
finally:
os.environ.clear()
os.environ.update(old_env)
class TestCreateApp:
"""Tests for the create_app factory function."""
def test_creates_app_with_provided_settings(self, seeded_db):
"""create_app(settings=...) uses provided settings."""
from animaltrack.web.app import create_app
settings = make_test_settings()
app, rt = create_app(settings=settings, db=seeded_db)
assert app is not None
assert rt is not None
assert app.state.settings is settings
assert app.state.db is seeded_db
def test_app_has_db_on_state(self, seeded_db):
"""Database accessible via app.state.db."""
from animaltrack.web.app import create_app
settings = make_test_settings()
app, rt = create_app(settings=settings, db=seeded_db)
assert hasattr(app.state, "db")
assert app.state.db is seeded_db
def test_app_has_settings_on_state(self, seeded_db):
"""Settings accessible via app.state.settings."""
from animaltrack.web.app import create_app
settings = make_test_settings()
app, rt = create_app(settings=settings, db=seeded_db)
assert hasattr(app.state, "settings")
assert app.state.settings.csrf_secret == "test-secret"
class TestAppWithTestClient:
"""Integration tests using Starlette TestClient."""
@pytest.fixture
def client(self, seeded_db):
"""Create a test client for the app."""
from starlette.testclient import TestClient
from animaltrack.web.app import create_app
# TestClient uses 'testclient' as the host, so we need to trust it
settings = make_test_settings(trusted_proxy_ips="testclient")
app, rt = create_app(settings=settings, db=seeded_db)
return TestClient(app, raise_server_exceptions=False)
def test_healthz_returns_200(self, client):
"""GET /healthz returns 200 OK."""
resp = client.get("/healthz")
assert resp.status_code == 200
def test_unauthenticated_route_returns_401(self, client):
"""Protected route without auth returns 401."""
# Any route that requires auth
resp = client.get("/")
assert resp.status_code == 401
def test_authenticated_request_succeeds(self, client):
"""Request with valid auth header succeeds."""
resp = client.get(
"/",
headers={"X-Oidc-Username": "ppetru"},
)
# Should get a valid response (200 or 404 if route not implemented yet)
# The key is it shouldn't be 401
assert resp.status_code != 401
def test_untrusted_proxy_returns_403(self, seeded_db):
"""Request from untrusted IP returns 403."""
from starlette.testclient import TestClient
from animaltrack.web.app import create_app
# Configure with a different trusted IP (not 'testclient')
settings = make_test_settings(trusted_proxy_ips="10.0.0.1")
app, rt = create_app(settings=settings, db=seeded_db)
client = TestClient(app, raise_server_exceptions=False)
resp = client.get("/", headers={"X-Oidc-Username": "ppetru"})
# Should fail because TestClient uses host 'testclient', not in trusted list
assert resp.status_code == 403
assert b"not from trusted proxy" in resp.content
def test_csrf_required_on_post(self, client):
"""POST without CSRF token returns 403."""
# POST to / route - should fail CSRF check before reaching handler
resp = client.post(
"/",
headers={"X-Oidc-Username": "ppetru"},
)
assert resp.status_code == 403
assert b"CSRF" in resp.content
def test_csrf_with_valid_tokens_succeeds(self, client):
"""POST with matching CSRF tokens proceeds."""
csrf_token = "test-csrf-token-123"
client.cookies.set("csrf_token", csrf_token)
resp = client.post(
"/",
headers={
"X-Oidc-Username": "ppetru",
"X-CSRF-Token": csrf_token,
"Origin": "http://testserver",
},
)
# Should get through CSRF check (200 or 405 if method not allowed)
# The key is it shouldn't be 403 CSRF error
assert resp.status_code != 403 or b"CSRF" not in resp.content