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>
220 lines
7.5 KiB
Python
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
|