# 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