""" Pytest configuration and shared fixtures for DocuElevate tests. """ import os import tempfile from typing import Dict, Generator, Optional import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool # Capture original environment variables before overriding with test defaults. # This allows integration tests to detect when real API credentials are available # (e.g., injected via GitHub Actions secrets) and run live API verification. _EXTERNAL_API_ENV_KEYS = [ "OPENAI_API_KEY", "OPENAI_BASE_URL", "AZURE_AI_KEY", "AZURE_ENDPOINT", "AZURE_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "S3_BUCKET_NAME", "S3_FOLDER_PREFIX", "DROPBOX_APP_KEY", "DROPBOX_APP_SECRET", "DROPBOX_REFRESH_TOKEN", "ONEDRIVE_CLIENT_ID", "ONEDRIVE_CLIENT_SECRET", "ONEDRIVE_REFRESH_TOKEN", "ONEDRIVE_TENANT_ID", "ONEDRIVE_FOLDER_PATH", "GOOGLE_DRIVE_CREDENTIALS_JSON", "GOOGLE_DRIVE_FOLDER_ID", "AUTHENTIK_CLIENT_ID", "AUTHENTIK_CLIENT_SECRET", "AUTHENTIK_CONFIG_URL", "SESSION_SECRET", ] _PLACEHOLDER_VALUES = {"test-key", "test", "", "NOT_SET"} _original_env: Dict[str, Optional[str]] = {key: os.environ.get(key) for key in _EXTERNAL_API_ENV_KEYS} # Set test environment variables before importing app os.environ["DATABASE_URL"] = "sqlite:///:memory:" os.environ["REDIS_URL"] = "redis://localhost:6379/1" os.environ["OPENAI_API_KEY"] = "test-key" os.environ["AZURE_AI_KEY"] = "test-key" os.environ["AZURE_REGION"] = "test" os.environ["AZURE_ENDPOINT"] = "https://test.cognitiveservices.azure.com/" os.environ["GOTENBERG_URL"] = "http://localhost:3000" os.environ["WORKDIR"] = "/tmp" os.environ["AUTH_ENABLED"] = "False" os.environ["SESSION_SECRET"] = "test_secret_key_for_testing_must_be_at_least_32_characters_long" from app.database import Base # noqa: E402 from app.main import app as fastapi_app # noqa: E402 # Import models to register them with SQLAlchemy Base from app.models import ( # noqa: F401, E402 ApiToken, AuditLog, AutomationHook, ClassificationRuleModel, ComplianceTemplate, DocumentMetadata, FileRecord, Pipeline, PipelineRoutingRule, PipelineStep, ProcessingLog, SavedSearch, ScheduledJob, UserImapAccount, UserIntegration, UserProfile, WebhookConfig, ) @pytest.fixture(scope="session") def test_workdir() -> Generator[str, None, None]: """Create a temporary work directory for tests.""" with tempfile.TemporaryDirectory() as tmpdir: yield tmpdir @pytest.fixture(scope="function") def db_session(): """Create a fresh database session for each test.""" # Create an in-memory SQLite database engine = create_engine( "sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) # Create all tables Base.metadata.create_all(bind=engine) # Create a session TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = TestingSessionLocal() try: yield session finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.fixture(scope="function") def client(db_session) -> TestClient: """Create a test client with a fresh database.""" # Import the canonical get_db function from app.database import get_db from app.middleware.upload_rate_limit import require_upload_rate_limit # Override the get_db dependency to use our test database def override_get_db(): try: yield db_session finally: pass # Override the single canonical get_db dependency fastapi_app.dependency_overrides[get_db] = override_get_db # Disable per-user upload rate limiting in tests so that upload-heavy # test suites are not rejected with 429 Too Many Requests. async def _no_rate_limit() -> None: """No-op override: skip upload rate limiting during tests.""" return None fastapi_app.dependency_overrides[require_upload_rate_limit] = _no_rate_limit # Use base_url to satisfy TrustedHostMiddleware with TestClient(fastapi_app, base_url="http://localhost") as test_client: yield test_client # Clean up fastapi_app.dependency_overrides.clear() @pytest.fixture def sample_pdf_path(test_workdir) -> str: """Create a sample PDF file for testing.""" pdf_path = os.path.join(test_workdir, "test.pdf") # Create a minimal valid PDF pdf_content = b"""%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj xref 0 4 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n trailer << /Size 4 /Root 1 0 R >> startxref 197 %%EOF """ with open(pdf_path, "wb") as f: f.write(pdf_content) return pdf_path @pytest.fixture def sample_pdf_file(test_workdir): """Create a sample PDF file for testing, returning Path object.""" from pathlib import Path pdf_path = Path(test_workdir) / "test.pdf" # Create a minimal valid PDF pdf_content = b"""%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj xref 0 4 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n trailer << /Size 4 /Root 1 0 R >> startxref 197 %%EOF """ pdf_path.write_bytes(pdf_content) return pdf_path @pytest.fixture def sample_text_file(test_workdir) -> str: """Create a sample text file for testing.""" text_path = os.path.join(test_workdir, "test.txt") with open(text_path, "w") as f: f.write("This is a test document.\nWith multiple lines.\n") return text_path @pytest.fixture def mock_openai_response(): """Mock OpenAI API response for testing.""" return { "choices": [ { "message": { "content": '{"document_type": "invoice", "summary": "Test invoice", "tags": ["test", "invoice"]}' } } ] } @pytest.fixture def mock_azure_response(): """Mock Azure Document Intelligence API response for testing.""" return {"analyzeResult": {"content": "Test document content extracted by OCR", "pages": [{"pageNumber": 1}]}} def has_real_env(*keys: str) -> bool: """Check if real (non-placeholder) environment variables were set before test overrides. Returns True only if ALL specified keys had non-placeholder values in the original environment. Used by integration tests to decide whether to skip when real credentials are unavailable. """ for key in keys: value = _original_env.get(key) if value is None or value in _PLACEHOLDER_VALUES: return False return True @pytest.fixture(scope="session") def original_env() -> Dict[str, Optional[str]]: """Provide access to the original environment variables captured before test overrides.""" return dict(_original_env) # Markers for categorizing tests def pytest_configure(config): """Configure custom pytest markers.""" config.addinivalue_line("markers", "unit: Unit tests for individual functions/methods") config.addinivalue_line("markers", "integration: Integration tests for API endpoints and workflows") config.addinivalue_line("markers", "slow: Tests that take significant time to run") config.addinivalue_line("markers", "security: Security-related tests") config.addinivalue_line("markers", "requires_external: Tests requiring external services") config.addinivalue_line("markers", "requires_db: Tests requiring database") config.addinivalue_line("markers", "requires_redis: Tests requiring Redis") config.addinivalue_line("markers", "requires_docker: Tests requiring Docker") config.addinivalue_line("markers", "e2e: End-to-end tests with full infrastructure") # Import OAuth fixtures (must be at end to avoid circular imports) try: from tests.conftest_oauth import ( mock_oauth_server, oauth_config, oauth_enabled_app, oauth_test_token, test_user_info, use_real_oauth, ) # Make fixtures available __all__ = [ "mock_oauth_server", "oauth_config", "oauth_enabled_app", "oauth_test_token", "test_user_info", "use_real_oauth", ] except ImportError: # OAuth fixtures not available (testcontainers may not be installed) pass