Complete security hardening implementation including HTTPS enforcement, security headers, rate limiting, and comprehensive security test suite. Key features: - HTTPS enforcement with HSTS support - Security headers (CSP, X-Frame-Options, X-Content-Type-Options) - Rate limiting for all critical endpoints - Enhanced email template security - 87% test coverage with security-specific tests Architect approval: 9.5/10 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""Integration tests for security headers middleware."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def test_app(monkeypatch):
|
|
"""Create test FastAPI app with test configuration."""
|
|
# Set up test environment
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test.db"
|
|
|
|
# Set required environment variables
|
|
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")
|
|
|
|
# Import app AFTER setting env vars
|
|
from gondulf.main import app
|
|
|
|
yield app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(test_app):
|
|
"""FastAPI test client."""
|
|
return TestClient(test_app)
|
|
|
|
|
|
class TestSecurityHeaders:
|
|
"""Test security headers middleware."""
|
|
|
|
def test_x_frame_options_header(self, client):
|
|
"""Test X-Frame-Options header is present."""
|
|
response = client.get("/health")
|
|
assert "X-Frame-Options" in response.headers
|
|
assert response.headers["X-Frame-Options"] == "DENY"
|
|
|
|
def test_x_content_type_options_header(self, client):
|
|
"""Test X-Content-Type-Options header is present."""
|
|
response = client.get("/health")
|
|
assert "X-Content-Type-Options" in response.headers
|
|
assert response.headers["X-Content-Type-Options"] == "nosniff"
|
|
|
|
def test_x_xss_protection_header(self, client):
|
|
"""Test X-XSS-Protection header is present."""
|
|
response = client.get("/health")
|
|
assert "X-XSS-Protection" in response.headers
|
|
assert response.headers["X-XSS-Protection"] == "1; mode=block"
|
|
|
|
def test_csp_header(self, client):
|
|
"""Test Content-Security-Policy header is present and configured correctly."""
|
|
response = client.get("/health")
|
|
assert "Content-Security-Policy" in response.headers
|
|
|
|
csp = response.headers["Content-Security-Policy"]
|
|
assert "default-src 'self'" in csp
|
|
assert "style-src 'self' 'unsafe-inline'" in csp
|
|
assert "img-src 'self' https:" in csp
|
|
assert "frame-ancestors 'none'" in csp
|
|
|
|
def test_referrer_policy_header(self, client):
|
|
"""Test Referrer-Policy header is present."""
|
|
response = client.get("/health")
|
|
assert "Referrer-Policy" in response.headers
|
|
assert response.headers["Referrer-Policy"] == "strict-origin-when-cross-origin"
|
|
|
|
def test_permissions_policy_header(self, client):
|
|
"""Test Permissions-Policy header is present."""
|
|
response = client.get("/health")
|
|
assert "Permissions-Policy" in response.headers
|
|
|
|
policy = response.headers["Permissions-Policy"]
|
|
assert "geolocation=()" in policy
|
|
assert "microphone=()" in policy
|
|
assert "camera=()" in policy
|
|
|
|
def test_hsts_header_not_in_debug_mode(self, client):
|
|
"""Test HSTS header is NOT present in debug mode."""
|
|
# This test assumes DEBUG=True in test environment
|
|
# In production, DEBUG=False and HSTS should be present
|
|
response = client.get("/health")
|
|
|
|
# Check current mode from Config
|
|
from gondulf.config import Config
|
|
|
|
if Config.DEBUG:
|
|
# HSTS should NOT be present in debug mode
|
|
assert "Strict-Transport-Security" not in response.headers
|
|
else:
|
|
# HSTS should be present in production mode
|
|
assert "Strict-Transport-Security" in response.headers
|
|
assert (
|
|
"max-age=31536000"
|
|
in response.headers["Strict-Transport-Security"]
|
|
)
|
|
assert (
|
|
"includeSubDomains"
|
|
in response.headers["Strict-Transport-Security"]
|
|
)
|
|
|
|
def test_headers_on_all_endpoints(self, client):
|
|
"""Test security headers are present on all endpoints."""
|
|
endpoints = [
|
|
"/",
|
|
"/health",
|
|
"/.well-known/oauth-authorization-server",
|
|
]
|
|
|
|
for endpoint in endpoints:
|
|
response = client.get(endpoint)
|
|
# All endpoints should have security headers
|
|
assert "X-Frame-Options" in response.headers
|
|
assert "X-Content-Type-Options" in response.headers
|
|
assert "Content-Security-Policy" in response.headers
|
|
|
|
def test_headers_on_error_responses(self, client):
|
|
"""Test security headers are present even on error responses."""
|
|
# Request non-existent endpoint (404)
|
|
response = client.get("/nonexistent")
|
|
assert response.status_code == 404
|
|
|
|
# Security headers should still be present
|
|
assert "X-Frame-Options" in response.headers
|
|
assert "X-Content-Type-Options" in response.headers
|