""" Integration tests for middleware chain. Tests that security headers and HTTPS enforcement middleware work together. """ import pytest from fastapi.testclient import TestClient @pytest.fixture def middleware_app_debug(monkeypatch, tmp_path): """Create app in debug mode for middleware testing.""" db_path = tmp_path / "test.db" monkeypatch.setenv("GONDULF_SECRET_KEY", "a" * 32) monkeypatch.setenv("GONDULF_BASE_URL", "https://auth.example.com") monkeypatch.setenv("GONDULF_DATABASE_URL", f"sqlite:///{db_path}") monkeypatch.setenv("GONDULF_DEBUG", "true") from gondulf.main import app return app @pytest.fixture def middleware_app_production(monkeypatch, tmp_path): """Create app in production mode for middleware testing.""" db_path = tmp_path / "test.db" monkeypatch.setenv("GONDULF_SECRET_KEY", "a" * 32) monkeypatch.setenv("GONDULF_BASE_URL", "https://auth.example.com") monkeypatch.setenv("GONDULF_DATABASE_URL", f"sqlite:///{db_path}") monkeypatch.setenv("GONDULF_DEBUG", "false") from gondulf.main import app return app @pytest.fixture def debug_client(middleware_app_debug): """Test client in debug mode.""" with TestClient(middleware_app_debug) as client: yield client @pytest.fixture def production_client(middleware_app_production): """Test client in production mode.""" with TestClient(middleware_app_production) as client: yield client class TestSecurityHeadersChain: """Tests for security headers middleware.""" def test_all_security_headers_present(self, debug_client): """Test all required security headers are present.""" response = debug_client.get("/") # Required security headers assert response.headers["X-Frame-Options"] == "DENY" assert response.headers["X-Content-Type-Options"] == "nosniff" assert response.headers["X-XSS-Protection"] == "1; mode=block" assert "Content-Security-Policy" in response.headers assert "Referrer-Policy" in response.headers assert "Permissions-Policy" in response.headers def test_csp_header_format(self, debug_client): """Test CSP header has correct format.""" response = debug_client.get("/") csp = response.headers["Content-Security-Policy"] assert "default-src 'self'" in csp assert "frame-ancestors 'none'" in csp def test_referrer_policy_value(self, debug_client): """Test Referrer-Policy has correct value.""" response = debug_client.get("/") assert response.headers["Referrer-Policy"] == "strict-origin-when-cross-origin" def test_permissions_policy_value(self, debug_client): """Test Permissions-Policy disables unnecessary features.""" response = debug_client.get("/") permissions = response.headers["Permissions-Policy"] assert "geolocation=()" in permissions assert "microphone=()" in permissions assert "camera=()" in permissions def test_hsts_not_in_debug_mode(self, debug_client): """Test HSTS header is not present in debug mode.""" response = debug_client.get("/") # HSTS should not be set in debug mode assert "Strict-Transport-Security" not in response.headers class TestMiddlewareOnAllEndpoints: """Tests that middleware applies to all endpoints.""" @pytest.mark.parametrize("endpoint", [ "/", "/health", "/.well-known/oauth-authorization-server", ]) def test_security_headers_on_endpoint(self, debug_client, endpoint): """Test security headers present on various endpoints.""" response = debug_client.get(endpoint) assert "X-Frame-Options" in response.headers assert "X-Content-Type-Options" in response.headers def test_security_headers_on_post_endpoint(self, debug_client): """Test security headers on POST endpoints.""" response = debug_client.post( "/api/verify/start", data={"me": "https://example.com"} ) assert "X-Frame-Options" in response.headers assert "X-Content-Type-Options" in response.headers def test_security_headers_on_error_response(self, debug_client): """Test security headers on 4xx error responses.""" response = debug_client.get("/authorize") # Missing required params assert response.status_code == 400 assert "X-Frame-Options" in response.headers assert "X-Content-Type-Options" in response.headers class TestHTTPSEnforcementMiddleware: """Tests for HTTPS enforcement middleware.""" def test_http_localhost_allowed_in_debug(self, debug_client): """Test HTTP to localhost is allowed in debug mode.""" # TestClient defaults to http response = debug_client.get("http://localhost/") # Should work in debug mode assert response.status_code == 200 def test_https_always_allowed(self, debug_client): """Test HTTPS requests are always allowed.""" response = debug_client.get("/") assert response.status_code == 200 class TestMiddlewareOrdering: """Tests for correct middleware ordering.""" def test_security_headers_applied_to_redirects(self, debug_client): """Test security headers are applied even on redirect responses.""" # This request should trigger a redirect due to error response = debug_client.get( "/authorize", params={ "client_id": "https://app.example.com", "redirect_uri": "https://app.example.com/callback", "response_type": "token", # Invalid - should redirect with error "state": "test" }, follow_redirects=False ) # Even on redirect, security headers should be present if response.status_code in (301, 302, 307, 308): assert "X-Frame-Options" in response.headers def test_middleware_chain_complete(self, debug_client): """Test full middleware chain processes correctly.""" response = debug_client.get("/") # Response should be successful assert response.status_code == 200 # Security headers from SecurityHeadersMiddleware assert "X-Frame-Options" in response.headers assert "X-Content-Type-Options" in response.headers # Application response should be JSON data = response.json() assert "service" in data class TestContentSecurityPolicy: """Tests for CSP header configuration.""" def test_csp_allows_self(self, debug_client): """Test CSP allows resources from same origin.""" response = debug_client.get("/") csp = response.headers["Content-Security-Policy"] assert "default-src 'self'" in csp def test_csp_allows_inline_styles(self, debug_client): """Test CSP allows inline styles for templates.""" response = debug_client.get("/") csp = response.headers["Content-Security-Policy"] assert "style-src" in csp assert "'unsafe-inline'" in csp def test_csp_allows_https_images(self, debug_client): """Test CSP allows HTTPS images for h-app logos.""" response = debug_client.get("/") csp = response.headers["Content-Security-Policy"] assert "img-src" in csp assert "https:" in csp def test_csp_prevents_framing(self, debug_client): """Test CSP prevents page from being framed.""" response = debug_client.get("/") csp = response.headers["Content-Security-Policy"] assert "frame-ancestors 'none'" in csp