Files
Gondulf/tests/integration/middleware/test_middleware_chain.py
Phil Skentelbery e1f79af347 feat(test): add Phase 5b integration and E2E tests
Add comprehensive integration and end-to-end test suites:
- Integration tests for API flows (authorization, token, verification)
- Integration tests for middleware chain and security headers
- Integration tests for domain verification services
- E2E tests for complete authentication flows
- E2E tests for error scenarios and edge cases
- Shared test fixtures and utilities in conftest.py
- Rename Dockerfile to Containerfile for Podman compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 22:22:04 -07:00

220 lines
7.5 KiB
Python

"""
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