Files
gh-christianlouis-docuelevate/tests/test_security_headers.py
T
2026-02-13 16:51:07 +00:00

368 lines
14 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.
"""
from unittest.mock import Mock
import pytest
@pytest.mark.unit
def test_security_headers_enabled_by_default(client):
"""Test that security headers are enabled by default."""
response = client.get("/")
# Should get a valid response (200, 302 redirect, or 404)
assert response.status_code in [200, 302, 404], f"Unexpected status code: {response.status_code}"
@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.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"]
# Note: ALLOW-FROM is deprecated in modern browsers; use CSP frame-ancestors instead
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.config import settings
from app.middleware.security_headers import SecurityHeadersMiddleware
# 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
@pytest.mark.unit
class TestSecurityHeadersMiddleware:
"""Tests for SecurityHeadersMiddleware class."""
def test_middleware_initialization(self):
"""Test middleware initializes with configuration."""
from app.config import settings
from app.middleware.security_headers import SecurityHeadersMiddleware
middleware = SecurityHeadersMiddleware(app=None, config=settings)
assert middleware.config == settings
assert middleware.enabled == settings.security_headers_enabled
@pytest.mark.asyncio
async def test_dispatch_adds_headers_when_enabled(self):
"""Test dispatch adds security headers when enabled."""
from fastapi import Response
from app.config import settings
from app.middleware.security_headers import SecurityHeadersMiddleware
if not settings.security_headers_enabled:
pytest.skip("Security headers disabled in configuration")
middleware = SecurityHeadersMiddleware(app=None, config=settings)
# Mock request and call_next
mock_request = Mock()
mock_response = Response(content="test", status_code=200)
async def mock_call_next(request):
return mock_response
result = await middleware.dispatch(mock_request, mock_call_next)
# At least some headers should be present
assert isinstance(result, Response)
@pytest.mark.asyncio
async def test_dispatch_skips_headers_when_disabled(self):
"""Test dispatch skips headers when disabled."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
# Create a config copy with headers disabled
mock_config = Mock()
mock_config.security_headers_enabled = False
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
mock_request = Mock()
mock_response = Response(content="test", status_code=200)
async def mock_call_next(request):
return mock_response
result = await middleware.dispatch(mock_request, mock_call_next)
# Headers should not be added
assert isinstance(result, Response)
def test_add_security_headers_hsts(self):
"""Test _add_security_headers adds HSTS header."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
mock_config = Mock()
mock_config.security_headers_enabled = True
mock_config.security_header_hsts_enabled = True
mock_config.security_header_hsts_value = "max-age=31536000; includeSubDomains"
mock_config.security_header_csp_enabled = False
mock_config.security_header_x_frame_options_enabled = False
mock_config.security_header_x_content_type_options_enabled = False
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
response = Response(content="test")
middleware._add_security_headers(response)
assert "Strict-Transport-Security" in response.headers
assert "max-age" in response.headers["Strict-Transport-Security"]
def test_add_security_headers_csp(self):
"""Test _add_security_headers adds CSP header."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
mock_config = Mock()
mock_config.security_headers_enabled = True
mock_config.security_header_hsts_enabled = False
mock_config.security_header_csp_enabled = True
mock_config.security_header_csp_value = "default-src 'self'; script-src 'self' 'unsafe-inline'"
mock_config.security_header_x_frame_options_enabled = False
mock_config.security_header_x_content_type_options_enabled = False
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
response = Response(content="test")
middleware._add_security_headers(response)
assert "Content-Security-Policy" in response.headers
assert "default-src" in response.headers["Content-Security-Policy"]
def test_add_security_headers_x_frame_options(self):
"""Test _add_security_headers adds X-Frame-Options header."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
mock_config = Mock()
mock_config.security_headers_enabled = True
mock_config.security_header_hsts_enabled = False
mock_config.security_header_csp_enabled = False
mock_config.security_header_x_frame_options_enabled = True
mock_config.security_header_x_frame_options_value = "DENY"
mock_config.security_header_x_content_type_options_enabled = False
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
response = Response(content="test")
middleware._add_security_headers(response)
assert "X-Frame-Options" in response.headers
assert response.headers["X-Frame-Options"] == "DENY"
def test_add_security_headers_x_content_type_options(self):
"""Test _add_security_headers adds X-Content-Type-Options header."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
mock_config = Mock()
mock_config.security_headers_enabled = True
mock_config.security_header_hsts_enabled = False
mock_config.security_header_csp_enabled = False
mock_config.security_header_x_frame_options_enabled = False
mock_config.security_header_x_content_type_options_enabled = True
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
response = Response(content="test")
middleware._add_security_headers(response)
assert "X-Content-Type-Options" in response.headers
assert response.headers["X-Content-Type-Options"] == "nosniff"
def test_add_all_security_headers(self):
"""Test _add_security_headers adds all headers when all enabled."""
from fastapi import Response
from app.middleware.security_headers import SecurityHeadersMiddleware
mock_config = Mock()
mock_config.security_headers_enabled = True
mock_config.security_header_hsts_enabled = True
mock_config.security_header_hsts_value = "max-age=31536000"
mock_config.security_header_csp_enabled = True
mock_config.security_header_csp_value = "default-src 'self'"
mock_config.security_header_x_frame_options_enabled = True
mock_config.security_header_x_frame_options_value = "SAMEORIGIN"
mock_config.security_header_x_content_type_options_enabled = True
middleware = SecurityHeadersMiddleware(app=None, config=mock_config)
response = Response(content="test")
middleware._add_security_headers(response)
assert "Strict-Transport-Security" in response.headers
assert "Content-Security-Policy" in response.headers
assert "X-Frame-Options" in response.headers
assert "X-Content-Type-Options" in response.headers