Files
gh-christianlouis-docuelevate/tests/test_security_headers.py
T
copilot-swe-agent[bot] e144fdd50a feat(security): add configurable security headers middleware
- Add SecurityHeadersMiddleware with HSTS, CSP, X-Frame-Options, X-Content-Type-Options
- Add configuration options in app/config.py
- Integrate middleware into app/main.py
- Add comprehensive tests in tests/test_security_headers.py
- Update .env.demo with security header examples
- Update docs/DeploymentGuide.md with security headers section and Traefik/Nginx examples
- Update docs/ConfigurationGuide.md with detailed configuration reference
- Update SECURITY_AUDIT.md to mark security headers implementation complete

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-02-10 14:05:19 +00:00

223 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""
Tests for security headers middleware.
These tests validate that security headers are properly added to HTTP responses
based on configuration settings.
"""
import pytest
from fastapi.testclient import TestClient
from app.config import Settings
from app.main import app
@pytest.fixture
def client():
"""Create a test client for the FastAPI app."""
return TestClient(app, base_url="http://testserver")
@pytest.mark.unit
def test_security_headers_enabled_by_default(client):
"""Test that security headers are enabled by default."""
response = client.get("/")
# At least one security header should be present
# We can't test all because some may be disabled individually
assert response.status_code in [200, 302, 404] # Valid status codes
@pytest.mark.unit
def test_hsts_header_present(client):
"""Test that HSTS header is present when enabled."""
from app.config import settings
# Skip test if HSTS is disabled
if not settings.security_headers_enabled or not settings.security_header_hsts_enabled:
pytest.skip("HSTS header is disabled in configuration")
response = client.get("/")
assert "Strict-Transport-Security" in response.headers
assert "max-age" in response.headers["Strict-Transport-Security"]
@pytest.mark.unit
def test_csp_header_present(client):
"""Test that CSP header is present when enabled."""
from app.config import settings
# Skip test if CSP is disabled
if not settings.security_headers_enabled or not settings.security_header_csp_enabled:
pytest.skip("CSP header is disabled in configuration")
response = client.get("/")
assert "Content-Security-Policy" in response.headers
assert "default-src" in response.headers["Content-Security-Policy"]
@pytest.mark.unit
def test_x_frame_options_header_present(client):
"""Test that X-Frame-Options header is present when enabled."""
from app.config import settings
# Skip test if X-Frame-Options is disabled
if not settings.security_headers_enabled or not settings.security_header_x_frame_options_enabled:
pytest.skip("X-Frame-Options header is disabled in configuration")
response = client.get("/")
assert "X-Frame-Options" in response.headers
assert response.headers["X-Frame-Options"] in ["DENY", "SAMEORIGIN"]
@pytest.mark.unit
def test_x_content_type_options_header_present(client):
"""Test that X-Content-Type-Options header is present when enabled."""
from app.config import settings
# Skip test if X-Content-Type-Options is disabled
if not settings.security_headers_enabled or not settings.security_header_x_content_type_options_enabled:
pytest.skip("X-Content-Type-Options header is disabled in configuration")
response = client.get("/")
assert "X-Content-Type-Options" in response.headers
assert response.headers["X-Content-Type-Options"] == "nosniff"
@pytest.mark.unit
def test_security_headers_on_api_endpoints(client):
"""Test that security headers are applied to API endpoints."""
from app.config import settings
if not settings.security_headers_enabled:
pytest.skip("Security headers are disabled in configuration")
response = client.get("/api/diagnostic/health")
# Check that at least some security headers are present
security_headers = [
"Strict-Transport-Security",
"Content-Security-Policy",
"X-Frame-Options",
"X-Content-Type-Options",
]
present_headers = [h for h in security_headers if h in response.headers]
assert len(present_headers) > 0, "No security headers found on API endpoint"
@pytest.mark.unit
def test_security_headers_on_static_files(client):
"""Test that security headers are applied to static file responses."""
from app.config import settings
if not settings.security_headers_enabled:
pytest.skip("Security headers are disabled in configuration")
# Try to access a static file (may not exist in test environment)
response = client.get("/static/logo.png")
# If file exists, check for security headers
if response.status_code == 200:
security_headers = [
"Strict-Transport-Security",
"Content-Security-Policy",
"X-Frame-Options",
"X-Content-Type-Options",
]
present_headers = [h for h in security_headers if h in response.headers]
assert len(present_headers) > 0, "No security headers found on static file"
@pytest.mark.security
def test_hsts_header_value_format(client):
"""Test that HSTS header has correct format."""
from app.config import settings
if not settings.security_headers_enabled or not settings.security_header_hsts_enabled:
pytest.skip("HSTS header is disabled in configuration")
response = client.get("/")
if "Strict-Transport-Security" in response.headers:
hsts_value = response.headers["Strict-Transport-Security"]
assert "max-age=" in hsts_value, "HSTS header missing max-age directive"
# Extract max-age value
parts = hsts_value.split(";")
max_age_part = [p.strip() for p in parts if p.strip().startswith("max-age=")]
assert len(max_age_part) > 0, "HSTS header missing max-age value"
@pytest.mark.security
def test_csp_header_value_format(client):
"""Test that CSP header has correct format."""
from app.config import settings
if not settings.security_headers_enabled or not settings.security_header_csp_enabled:
pytest.skip("CSP header is disabled in configuration")
response = client.get("/")
if "Content-Security-Policy" in response.headers:
csp_value = response.headers["Content-Security-Policy"]
# CSP should have at least a default-src directive
assert "default-src" in csp_value or "script-src" in csp_value, "CSP header missing required directives"
@pytest.mark.security
def test_x_frame_options_valid_value(client):
"""Test that X-Frame-Options header has valid value."""
from app.config import settings
if not settings.security_headers_enabled or not settings.security_header_x_frame_options_enabled:
pytest.skip("X-Frame-Options header is disabled in configuration")
response = client.get("/")
if "X-Frame-Options" in response.headers:
x_frame_value = response.headers["X-Frame-Options"]
valid_values = ["DENY", "SAMEORIGIN"]
assert (
x_frame_value in valid_values or x_frame_value.startswith("ALLOW-FROM")
), f"Invalid X-Frame-Options value: {x_frame_value}"
@pytest.mark.integration
def test_security_headers_configuration_loading():
"""Test that security header configuration is loaded correctly."""
from app.config import settings
# Verify that security header configuration attributes exist
assert hasattr(settings, "security_headers_enabled")
assert hasattr(settings, "security_header_hsts_enabled")
assert hasattr(settings, "security_header_hsts_value")
assert hasattr(settings, "security_header_csp_enabled")
assert hasattr(settings, "security_header_csp_value")
assert hasattr(settings, "security_header_x_frame_options_enabled")
assert hasattr(settings, "security_header_x_frame_options_value")
assert hasattr(settings, "security_header_x_content_type_options_enabled")
# Verify that boolean settings are actual booleans
assert isinstance(settings.security_headers_enabled, bool)
assert isinstance(settings.security_header_hsts_enabled, bool)
assert isinstance(settings.security_header_csp_enabled, bool)
assert isinstance(settings.security_header_x_frame_options_enabled, bool)
assert isinstance(settings.security_header_x_content_type_options_enabled, bool)
# Verify that string settings are actual strings
assert isinstance(settings.security_header_hsts_value, str)
assert isinstance(settings.security_header_csp_value, str)
assert isinstance(settings.security_header_x_frame_options_value, str)
@pytest.mark.integration
def test_middleware_respects_configuration():
"""Test that middleware respects individual header enable/disable settings."""
from app.middleware.security_headers import SecurityHeadersMiddleware
from app.config import settings
# Create middleware instance
middleware = SecurityHeadersMiddleware(app=None, config=settings)
# Verify that middleware stores configuration
assert middleware.config == settings
assert middleware.enabled == settings.security_headers_enabled