#!/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