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>
This commit is contained in:
1
tests/integration/middleware/__init__.py
Normal file
1
tests/integration/middleware/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Middleware integration tests for Gondulf IndieAuth server."""
|
||||
219
tests/integration/middleware/test_middleware_chain.py
Normal file
219
tests/integration/middleware/test_middleware_chain.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user