1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141"""Test configuration and fixtures.
IMPORTANT: Shared fixtures are now available from tests.conftest_fixtures:
- mock_redis_client, mock_s3_client, mock_ai_service (clients.py)
- generate_job_id, create_test_job, create_pii_queue_payload, etc. (data_factories.py)
- assert_job_state, assert_s3_upload, assert_redis_set (helpers.py)
Import these instead of defining duplicates in test files.
"""
# Configure test environment before any imports
# Force disable API key auth for integration tests
import os
os.environ["ENABLE_API_KEY_AUTH"] = "false"
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from src.main import app
from src.shared.models.queue import ProcessingQueuePayload
# Import shared fixtures to make them available to all tests
# Note: Using string-based plugin registration to avoid import-time side effects
pytest_plugins = ["tests.conftest_fixtures.clients", "tests.conftest_fixtures.data_factories"]
@pytest.fixture
def client():
"""Create test client.
Note: API key authentication is enabled by default in .env.
Tests that need to make API requests should use the api_key_headers fixture.
"""
return TestClient(app)
@pytest.fixture
def api_key_headers():
"""Generate API key headers for authenticated requests.
Returns headers dict with X-API-Key for use in test requests.
Uses the first API key from settings to match the configured environment.
Note: The API key must match what's in the environment when the app was initialized,
since APIKeyAuthMiddleware caches keys at startup.
"""
import os
# Read directly from environment to match what middleware cached at startup
api_keys_env = os.getenv("API_KEYS", "")
if api_keys_env:
keys = api_keys_env.split(",")
api_key = keys[0].strip() if keys else ""
else:
# Fallback for tests that don't require real auth
api_key = "test-key-fallback"
return {"X-API-Key": api_key}
@pytest.fixture
def sample_pdf():
"""Create sample PDF file for testing."""
# Simple PDF content
pdf_content = (
b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
b"3 0 obj\n<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 "
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> "
b"/MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n"
b"4 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n"
b"(Test PDF) Tj\nET\nendstream\nendobj\nxref\n0 5\n"
b"0000000000 65535 f\n0000000009 00000 n\n0000000058 00000 n\n"
b"0000000115 00000 n\n0000000317 00000 n\ntrailer\n"
b"<< /Size 5 /Root 1 0 R >>\nstartxref\n410\n%%EOF"
)
return pdf_content
# ============================================================================
# Core Pipeline Test Fixtures
# ============================================================================
@pytest.fixture
def sample_job_payload():
"""Sample processing queue payload for testing."""
return ProcessingQueuePayload(
job_id="550e8400-e29b-41d4-a716-446655440000",
s3_key="temp/550e8400-e29b-41d4-a716-446655440000/input.pdf",
approved_at=None,
)
@pytest.fixture
def mock_storage_service():
"""Mock StorageService for unit tests.
Uses MagicMock as container with AsyncMock for async methods.
This prevents unawaited coroutine warnings when tests override side_effect.
"""
mock = MagicMock()
mock.download_temp_file = AsyncMock(return_value=b"fake_pdf_content")
mock.upload_result = AsyncMock(return_value="s3://equalify-results/550e8400.../v20250101_120000/output.md")
# PRD-027: save_processing_result for new review checklist workflow
mock.save_processing_result = AsyncMock(return_value="processing-results/550e8400.../result.json")
# PRD-027: load_processing_result for retrieving saved results
mock.load_processing_result = AsyncMock(return_value=None)
return mock
@pytest.fixture
def mock_queue_service():
"""Mock QueueService for unit tests.
Uses MagicMock as container with AsyncMock for async methods.
This prevents unawaited coroutine warnings when tests access unmocked attributes.
"""
mock = MagicMock()
mock.enqueue = AsyncMock()
mock.dequeue = AsyncMock()
return mock
@pytest.fixture
def mock_job_service():
"""Mock JobService for unit tests.
Uses MagicMock as container with AsyncMock for async methods.
This prevents unawaited coroutine warnings when tests access unmocked attributes.
"""
mock = MagicMock()
mock.update_job_status = AsyncMock()
mock.get_job_status = AsyncMock(return_value="processing")
mock.get_job = AsyncMock(return_value=None) # For debug service check
return mock