#!/usr/bin/env python3 """ Tests for CORS middleware configuration. These tests validate that: - CORS middleware is disabled by default (reverse proxy handles CORS in production) - When enabled, CORS headers are present on responses - Configuration settings (allowed origins, methods, headers, credentials) are respected - Preflight (OPTIONS) requests are handled correctly when CORS is enabled """ import pytest @pytest.mark.integration def test_cors_configuration_exists(): """Test that CORS configuration attributes exist in settings.""" from app.config import settings assert hasattr(settings, "cors_enabled") assert hasattr(settings, "cors_allowed_origins") assert hasattr(settings, "cors_allow_credentials") assert hasattr(settings, "cors_allowed_methods") assert hasattr(settings, "cors_allowed_headers") @pytest.mark.integration def test_cors_disabled_by_default(): """Test that CORS is disabled by default (reverse proxy handles it).""" from app.config import Settings # The default value for cors_enabled should be False field_info = Settings.model_fields.get("cors_enabled") assert field_info is not None assert field_info.default is False, "CORS should be disabled by default" @pytest.mark.integration def test_cors_configuration_types(): """Test that CORS configuration settings have correct types.""" from app.config import settings assert isinstance(settings.cors_enabled, bool) assert isinstance(settings.cors_allowed_origins, list) assert isinstance(settings.cors_allow_credentials, bool) assert isinstance(settings.cors_allowed_methods, list) assert isinstance(settings.cors_allowed_headers, list) @pytest.mark.unit def test_cors_headers_absent_when_disabled(client): """Test that CORS headers are NOT present when middleware is disabled.""" from app.config import settings if settings.cors_enabled: pytest.skip("CORS is enabled in this test environment") response = client.get("/api/diagnostic/health", headers={"Origin": "https://example.com"}) # When CORS middleware is disabled, the application should not add CORS headers assert "access-control-allow-origin" not in response.headers @pytest.mark.unit def test_no_cors_preflight_when_disabled(client): """Test that preflight OPTIONS requests return 405 (or similar) when CORS is disabled.""" from app.config import settings if settings.cors_enabled: pytest.skip("CORS is enabled in this test environment") response = client.options( "/api/diagnostic/health", headers={ "Origin": "https://example.com", "Access-Control-Request-Method": "GET", }, ) # Without CORSMiddleware, OPTIONS preflight is not handled as CORS assert "access-control-allow-origin" not in response.headers @pytest.mark.unit class TestCORSMiddlewareEnabled: """Tests for CORSMiddleware behavior when enabled.""" def _make_app_with_cors(self, allow_origins=None, allow_credentials=False, allow_methods=None, allow_headers=None): """Create a minimal FastAPI app with CORSMiddleware for isolated testing.""" from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.testclient import TestClient test_app = FastAPI() test_app.add_middleware( CORSMiddleware, allow_origins=allow_origins or ["https://trusted.example.com"], allow_credentials=allow_credentials, allow_methods=allow_methods or ["GET", "POST", "OPTIONS"], allow_headers=allow_headers or ["*"], ) @test_app.get("/test") def test_endpoint(): return {"status": "ok"} return TestClient(test_app) def test_cors_header_present_for_allowed_origin(self): """Test that CORS header is present for allowed origin.""" test_client = self._make_app_with_cors(allow_origins=["https://trusted.example.com"]) response = test_client.get("/test", headers={"Origin": "https://trusted.example.com"}) assert response.status_code == 200 assert "access-control-allow-origin" in response.headers assert response.headers["access-control-allow-origin"] == "https://trusted.example.com" def test_cors_wildcard_origin(self): """Test that wildcard origin allows any origin.""" test_client = self._make_app_with_cors(allow_origins=["*"]) response = test_client.get("/test", headers={"Origin": "https://any-origin.example.com"}) assert response.status_code == 200 assert "access-control-allow-origin" in response.headers def test_cors_preflight_returns_200(self): """Test that CORS preflight OPTIONS request returns 200.""" test_client = self._make_app_with_cors(allow_origins=["https://trusted.example.com"]) response = test_client.options( "/test", headers={ "Origin": "https://trusted.example.com", "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "Content-Type", }, ) assert response.status_code == 200 assert "access-control-allow-origin" in response.headers def test_cors_credentials_allowed(self): """Test that credentials header is present when allow_credentials=True.""" test_client = self._make_app_with_cors( allow_origins=["https://trusted.example.com"], allow_credentials=True, ) response = test_client.get("/test", headers={"Origin": "https://trusted.example.com"}) assert response.status_code == 200 assert response.headers.get("access-control-allow-credentials") == "true" def test_cors_methods_in_preflight(self): """Test that allowed methods appear in preflight response.""" test_client = self._make_app_with_cors( allow_origins=["https://trusted.example.com"], allow_methods=["GET", "POST"], ) response = test_client.options( "/test", headers={ "Origin": "https://trusted.example.com", "Access-Control-Request-Method": "POST", }, ) assert response.status_code == 200 @pytest.mark.unit def test_cors_parse_comma_separated_origins(): """Test that comma-separated CORS origins string is parsed into a list.""" import os # Temporarily set env var to test parsing original = os.environ.get("CORS_ALLOWED_ORIGINS") os.environ["CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com" try: from app.config import Settings test_settings = Settings( database_url="sqlite:///:memory:", redis_url="redis://localhost:6379/0", openai_api_key="test-key", azure_ai_key="test-key", azure_region="test", azure_endpoint="https://test.cognitiveservices.azure.com/", gotenberg_url="http://localhost:3000", workdir="/tmp", auth_enabled=False, ) assert isinstance(test_settings.cors_allowed_origins, list) assert len(test_settings.cors_allowed_origins) == 2 origins = set(test_settings.cors_allowed_origins) assert origins == {"https://app.example.com", "https://admin.example.com"} finally: if original is None: os.environ.pop("CORS_ALLOWED_ORIGINS", None) else: os.environ["CORS_ALLOWED_ORIGINS"] = original @pytest.mark.unit def test_cors_single_origin_string_to_list(): """Test that a single-origin string is parsed into a list with one item.""" import os original = os.environ.get("CORS_ALLOWED_ORIGINS") os.environ["CORS_ALLOWED_ORIGINS"] = "https://app.example.com" try: from app.config import Settings test_settings = Settings( database_url="sqlite:///:memory:", redis_url="redis://localhost:6379/0", openai_api_key="test-key", azure_ai_key="test-key", azure_region="test", azure_endpoint="https://test.cognitiveservices.azure.com/", gotenberg_url="http://localhost:3000", workdir="/tmp", auth_enabled=False, ) assert isinstance(test_settings.cors_allowed_origins, list) assert len(test_settings.cors_allowed_origins) == 1 assert test_settings.cors_allowed_origins[0] == "https://app.example.com" finally: if original is None: os.environ.pop("CORS_ALLOWED_ORIGINS", None) else: os.environ["CORS_ALLOWED_ORIGINS"] = original