bc435ed8cb
- Enhanced test_upload_email.py: Added 20+ tests for email upload task (get_email_template, extract_metadata, attach_logo, prepare_recipients, send_email, upload_to_email) - Enhanced test_api_settings.py: Added 10+ tests for settings API endpoints and models - Created test_upload_google_drive.py: Added 25+ tests for Google Drive upload (OAuth, service account, metadata, truncation) - Enhanced test_views_settings.py: Added 15+ tests for settings view and admin access - Enhanced test_upload_ftp_additional.py: Added 18+ tests for FTP upload (FTPS, plaintext, directory creation, error handling) - Enhanced test_security_headers.py: Added 12+ tests for security headers middleware - Enhanced test_check_credentials.py: Added 15+ tests for credential checking task - Enhanced test_views_status.py: Added 15+ tests for status dashboard and env debug views Target: Reach ≥80% coverage for 9 modules Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
362 lines
14 KiB
Python
362 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 app.config import settings
|
|
from app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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 app.config import settings
|
|
from app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
# 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 app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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 app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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 app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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 app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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 app.middleware.security_headers import SecurityHeadersMiddleware
|
|
from fastapi import Response
|
|
|
|
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
|