karaoke-gen 0.86.7__py3-none-any.whl → 0.96.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/style_loader.py +3 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for admin email endpoints.
|
|
3
|
+
|
|
4
|
+
Tests the completion message and send email API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
|
|
11
|
+
from backend.api.routes.admin import router
|
|
12
|
+
from backend.api.dependencies import require_admin
|
|
13
|
+
from backend.models.job import Job, JobStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Create a test app with the admin router
|
|
17
|
+
# The router already has prefix="/admin", so we add /api prefix
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
app.include_router(router, prefix="/api")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_mock_admin():
|
|
23
|
+
"""Override for require_admin dependency."""
|
|
24
|
+
return ("admin@example.com", "admin", 1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Override the require_admin dependency
|
|
28
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def client():
|
|
33
|
+
"""Create a test client."""
|
|
34
|
+
return TestClient(app)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def mock_job():
|
|
39
|
+
"""Create a mock job."""
|
|
40
|
+
job = Mock(spec=Job)
|
|
41
|
+
job.job_id = "test-job-123"
|
|
42
|
+
job.user_email = "user@example.com"
|
|
43
|
+
job.artist = "Test Artist"
|
|
44
|
+
job.title = "Test Song"
|
|
45
|
+
job.status = JobStatus.COMPLETE
|
|
46
|
+
job.audio_hash = "hash123"
|
|
47
|
+
job.review_token = "review123"
|
|
48
|
+
job.instrumental_token = "inst123"
|
|
49
|
+
job.state_data = {
|
|
50
|
+
"youtube_url": "https://youtube.com/watch?v=test123",
|
|
51
|
+
"dropbox_link": "https://dropbox.com/folder/test",
|
|
52
|
+
"brand_code": "NOMAD-1234",
|
|
53
|
+
}
|
|
54
|
+
return job
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestGetCompletionMessage:
|
|
58
|
+
"""Tests for GET /api/admin/jobs/{job_id}/completion-message endpoint."""
|
|
59
|
+
|
|
60
|
+
def test_returns_completion_message(self, client, mock_job):
|
|
61
|
+
"""Test successful completion message retrieval."""
|
|
62
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
63
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns:
|
|
64
|
+
|
|
65
|
+
# Setup mocks
|
|
66
|
+
mock_jm = Mock()
|
|
67
|
+
mock_jm.get_job.return_value = mock_job
|
|
68
|
+
mock_jm_class.return_value = mock_jm
|
|
69
|
+
|
|
70
|
+
mock_ns = Mock()
|
|
71
|
+
mock_ns.get_completion_message.return_value = "Your video is ready!"
|
|
72
|
+
mock_get_ns.return_value = mock_ns
|
|
73
|
+
|
|
74
|
+
response = client.get(
|
|
75
|
+
"/api/admin/jobs/test-job-123/completion-message",
|
|
76
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert response.status_code == 200
|
|
80
|
+
data = response.json()
|
|
81
|
+
assert data["job_id"] == "test-job-123"
|
|
82
|
+
assert data["message"] == "Your video is ready!"
|
|
83
|
+
# Subject format: "NOMAD-1234: Test Artist - Test Song (Your karaoke video is ready!)"
|
|
84
|
+
assert data["subject"] == "NOMAD-1234: Test Artist - Test Song (Your karaoke video is ready!)"
|
|
85
|
+
assert data["youtube_url"] == "https://youtube.com/watch?v=test123"
|
|
86
|
+
assert data["dropbox_url"] == "https://dropbox.com/folder/test"
|
|
87
|
+
|
|
88
|
+
def test_returns_404_when_job_not_found(self, client):
|
|
89
|
+
"""Test 404 when job doesn't exist."""
|
|
90
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
91
|
+
mock_jm = Mock()
|
|
92
|
+
mock_jm.get_job.return_value = None
|
|
93
|
+
mock_jm_class.return_value = mock_jm
|
|
94
|
+
|
|
95
|
+
response = client.get(
|
|
96
|
+
"/api/admin/jobs/nonexistent-job/completion-message",
|
|
97
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert response.status_code == 404
|
|
101
|
+
|
|
102
|
+
def test_handles_none_state_data(self, client, mock_job):
|
|
103
|
+
"""Test handling of job with None state_data."""
|
|
104
|
+
mock_job.state_data = None
|
|
105
|
+
|
|
106
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
107
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns:
|
|
108
|
+
|
|
109
|
+
mock_jm = Mock()
|
|
110
|
+
mock_jm.get_job.return_value = mock_job
|
|
111
|
+
mock_jm_class.return_value = mock_jm
|
|
112
|
+
|
|
113
|
+
mock_ns = Mock()
|
|
114
|
+
mock_ns.get_completion_message.return_value = "Your video is ready!"
|
|
115
|
+
mock_get_ns.return_value = mock_ns
|
|
116
|
+
|
|
117
|
+
response = client.get(
|
|
118
|
+
"/api/admin/jobs/test-job-123/completion-message",
|
|
119
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert response.status_code == 200
|
|
123
|
+
data = response.json()
|
|
124
|
+
assert data["youtube_url"] is None
|
|
125
|
+
assert data["dropbox_url"] is None
|
|
126
|
+
|
|
127
|
+
def test_default_subject_without_song_info(self, client, mock_job):
|
|
128
|
+
"""Test default subject when no artist/title."""
|
|
129
|
+
mock_job.artist = None
|
|
130
|
+
mock_job.title = None
|
|
131
|
+
|
|
132
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
133
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns:
|
|
134
|
+
|
|
135
|
+
mock_jm = Mock()
|
|
136
|
+
mock_jm.get_job.return_value = mock_job
|
|
137
|
+
mock_jm_class.return_value = mock_jm
|
|
138
|
+
|
|
139
|
+
mock_ns = Mock()
|
|
140
|
+
mock_ns.get_completion_message.return_value = "Message"
|
|
141
|
+
mock_get_ns.return_value = mock_ns
|
|
142
|
+
|
|
143
|
+
response = client.get(
|
|
144
|
+
"/api/admin/jobs/test-job-123/completion-message",
|
|
145
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert response.status_code == 200
|
|
149
|
+
data = response.json()
|
|
150
|
+
assert data["subject"] == "Your karaoke video is ready!"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestSendCompletionEmail:
|
|
154
|
+
"""Tests for POST /api/admin/jobs/{job_id}/send-completion-email endpoint."""
|
|
155
|
+
|
|
156
|
+
def test_sends_email_successfully(self, client, mock_job):
|
|
157
|
+
"""Test successful email sending."""
|
|
158
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
159
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns, \
|
|
160
|
+
patch('backend.services.email_service.get_email_service') as mock_get_es:
|
|
161
|
+
|
|
162
|
+
mock_jm = Mock()
|
|
163
|
+
mock_jm.get_job.return_value = mock_job
|
|
164
|
+
mock_jm_class.return_value = mock_jm
|
|
165
|
+
|
|
166
|
+
mock_ns = Mock()
|
|
167
|
+
mock_ns.get_completion_message.return_value = "Your video is ready!"
|
|
168
|
+
mock_get_ns.return_value = mock_ns
|
|
169
|
+
|
|
170
|
+
mock_es = Mock()
|
|
171
|
+
mock_es.send_job_completion.return_value = True
|
|
172
|
+
mock_get_es.return_value = mock_es
|
|
173
|
+
|
|
174
|
+
response = client.post(
|
|
175
|
+
"/api/admin/jobs/test-job-123/send-completion-email",
|
|
176
|
+
json={"to_email": "customer@example.com", "cc_admin": True},
|
|
177
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert response.status_code == 200
|
|
181
|
+
data = response.json()
|
|
182
|
+
assert data["success"] is True
|
|
183
|
+
assert data["job_id"] == "test-job-123"
|
|
184
|
+
assert data["to_email"] == "customer@example.com"
|
|
185
|
+
|
|
186
|
+
# Verify email was sent with correct params
|
|
187
|
+
mock_es.send_job_completion.assert_called_once_with(
|
|
188
|
+
to_email="customer@example.com",
|
|
189
|
+
message_content="Your video is ready!",
|
|
190
|
+
artist="Test Artist",
|
|
191
|
+
title="Test Song",
|
|
192
|
+
brand_code="NOMAD-1234",
|
|
193
|
+
cc_admin=True,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def test_sends_email_without_cc(self, client, mock_job):
|
|
197
|
+
"""Test email sending without CC."""
|
|
198
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
199
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns, \
|
|
200
|
+
patch('backend.services.email_service.get_email_service') as mock_get_es:
|
|
201
|
+
|
|
202
|
+
mock_jm = Mock()
|
|
203
|
+
mock_jm.get_job.return_value = mock_job
|
|
204
|
+
mock_jm_class.return_value = mock_jm
|
|
205
|
+
|
|
206
|
+
mock_ns = Mock()
|
|
207
|
+
mock_ns.get_completion_message.return_value = "Message"
|
|
208
|
+
mock_get_ns.return_value = mock_ns
|
|
209
|
+
|
|
210
|
+
mock_es = Mock()
|
|
211
|
+
mock_es.send_job_completion.return_value = True
|
|
212
|
+
mock_get_es.return_value = mock_es
|
|
213
|
+
|
|
214
|
+
response = client.post(
|
|
215
|
+
"/api/admin/jobs/test-job-123/send-completion-email",
|
|
216
|
+
json={"to_email": "customer@example.com", "cc_admin": False},
|
|
217
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
assert response.status_code == 200
|
|
221
|
+
|
|
222
|
+
# Verify CC was not included
|
|
223
|
+
call_kwargs = mock_es.send_job_completion.call_args.kwargs
|
|
224
|
+
assert call_kwargs["cc_admin"] is False
|
|
225
|
+
|
|
226
|
+
def test_returns_404_when_job_not_found(self, client):
|
|
227
|
+
"""Test 404 when job doesn't exist."""
|
|
228
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
229
|
+
mock_jm = Mock()
|
|
230
|
+
mock_jm.get_job.return_value = None
|
|
231
|
+
mock_jm_class.return_value = mock_jm
|
|
232
|
+
|
|
233
|
+
response = client.post(
|
|
234
|
+
"/api/admin/jobs/nonexistent-job/send-completion-email",
|
|
235
|
+
json={"to_email": "customer@example.com"},
|
|
236
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
assert response.status_code == 404
|
|
240
|
+
|
|
241
|
+
def test_returns_500_when_email_fails(self, client, mock_job):
|
|
242
|
+
"""Test 500 when email sending fails."""
|
|
243
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
244
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns, \
|
|
245
|
+
patch('backend.services.email_service.get_email_service') as mock_get_es:
|
|
246
|
+
|
|
247
|
+
mock_jm = Mock()
|
|
248
|
+
mock_jm.get_job.return_value = mock_job
|
|
249
|
+
mock_jm_class.return_value = mock_jm
|
|
250
|
+
|
|
251
|
+
mock_ns = Mock()
|
|
252
|
+
mock_ns.get_completion_message.return_value = "Message"
|
|
253
|
+
mock_get_ns.return_value = mock_ns
|
|
254
|
+
|
|
255
|
+
mock_es = Mock()
|
|
256
|
+
mock_es.send_job_completion.return_value = False # Email failed
|
|
257
|
+
mock_get_es.return_value = mock_es
|
|
258
|
+
|
|
259
|
+
response = client.post(
|
|
260
|
+
"/api/admin/jobs/test-job-123/send-completion-email",
|
|
261
|
+
json={"to_email": "customer@example.com"},
|
|
262
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
assert response.status_code == 500
|
|
266
|
+
assert "Failed to send email" in response.json()["detail"]
|
|
267
|
+
|
|
268
|
+
def test_handles_none_state_data(self, client, mock_job):
|
|
269
|
+
"""Test handling of job with None state_data."""
|
|
270
|
+
mock_job.state_data = None
|
|
271
|
+
|
|
272
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
273
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns, \
|
|
274
|
+
patch('backend.services.email_service.get_email_service') as mock_get_es:
|
|
275
|
+
|
|
276
|
+
mock_jm = Mock()
|
|
277
|
+
mock_jm.get_job.return_value = mock_job
|
|
278
|
+
mock_jm_class.return_value = mock_jm
|
|
279
|
+
|
|
280
|
+
mock_ns = Mock()
|
|
281
|
+
mock_ns.get_completion_message.return_value = "Message"
|
|
282
|
+
mock_get_ns.return_value = mock_ns
|
|
283
|
+
|
|
284
|
+
mock_es = Mock()
|
|
285
|
+
mock_es.send_job_completion.return_value = True
|
|
286
|
+
mock_get_es.return_value = mock_es
|
|
287
|
+
|
|
288
|
+
response = client.post(
|
|
289
|
+
"/api/admin/jobs/test-job-123/send-completion-email",
|
|
290
|
+
json={"to_email": "customer@example.com"},
|
|
291
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
assert response.status_code == 200
|
|
295
|
+
|
|
296
|
+
def test_accepts_any_email_string(self, client, mock_job):
|
|
297
|
+
"""Test that any string is currently accepted as email (no validation).
|
|
298
|
+
|
|
299
|
+
Documents current behavior: FastAPI/Pydantic doesn't validate email
|
|
300
|
+
format by default. If validation is added later, this test should
|
|
301
|
+
be updated to expect 422 for invalid emails.
|
|
302
|
+
"""
|
|
303
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
304
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns, \
|
|
305
|
+
patch('backend.services.email_service.get_email_service') as mock_get_es:
|
|
306
|
+
|
|
307
|
+
mock_jm = Mock()
|
|
308
|
+
mock_jm.get_job.return_value = mock_job
|
|
309
|
+
mock_jm_class.return_value = mock_jm
|
|
310
|
+
|
|
311
|
+
mock_ns = Mock()
|
|
312
|
+
mock_ns.get_completion_message.return_value = "Message"
|
|
313
|
+
mock_get_ns.return_value = mock_ns
|
|
314
|
+
|
|
315
|
+
mock_es = Mock()
|
|
316
|
+
mock_es.send_job_completion.return_value = True
|
|
317
|
+
mock_get_es.return_value = mock_es
|
|
318
|
+
|
|
319
|
+
response = client.post(
|
|
320
|
+
"/api/admin/jobs/test-job-123/send-completion-email",
|
|
321
|
+
json={"to_email": "invalid-email"}, # Not a valid email format
|
|
322
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Currently accepts any string - endpoint succeeds
|
|
326
|
+
assert response.status_code == 200
|
|
327
|
+
# Verify the invalid email was passed through
|
|
328
|
+
mock_es.send_job_completion.assert_called_once()
|
|
329
|
+
call_kwargs = mock_es.send_job_completion.call_args.kwargs
|
|
330
|
+
assert call_kwargs["to_email"] == "invalid-email"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TestIdleReminderEndpoint:
|
|
334
|
+
"""Tests for the internal idle reminder endpoint."""
|
|
335
|
+
|
|
336
|
+
def test_sends_reminder_when_still_idle(self, mock_job):
|
|
337
|
+
"""Test that reminder is sent when user is still idle."""
|
|
338
|
+
from backend.api.routes.internal import router as internal_router
|
|
339
|
+
from backend.api.dependencies import require_admin
|
|
340
|
+
|
|
341
|
+
# Create test app for internal router
|
|
342
|
+
# The router already has prefix="/internal", so we add /api prefix
|
|
343
|
+
test_app = FastAPI()
|
|
344
|
+
test_app.include_router(internal_router, prefix="/api")
|
|
345
|
+
test_app.dependency_overrides[require_admin] = get_mock_admin
|
|
346
|
+
|
|
347
|
+
mock_job.status = JobStatus.AWAITING_REVIEW.value # Use .value for string comparison
|
|
348
|
+
mock_job.state_data = {
|
|
349
|
+
"blocking_state_entered_at": "2024-01-01T00:00:00",
|
|
350
|
+
"blocking_action_type": "lyrics",
|
|
351
|
+
"reminder_sent": False,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
with patch('backend.api.routes.internal.JobManager') as mock_jm_class, \
|
|
355
|
+
patch('backend.services.job_notification_service.get_job_notification_service') as mock_get_ns:
|
|
356
|
+
|
|
357
|
+
mock_jm = Mock()
|
|
358
|
+
mock_jm.get_job.return_value = mock_job
|
|
359
|
+
mock_jm.firestore = Mock() # Mock firestore for update_job call
|
|
360
|
+
mock_jm_class.return_value = mock_jm
|
|
361
|
+
|
|
362
|
+
mock_ns = Mock()
|
|
363
|
+
mock_ns.send_action_reminder_email = AsyncMock(return_value=True)
|
|
364
|
+
mock_get_ns.return_value = mock_ns
|
|
365
|
+
|
|
366
|
+
test_client = TestClient(test_app)
|
|
367
|
+
response = test_client.post(
|
|
368
|
+
"/api/internal/jobs/test-job-123/check-idle-reminder",
|
|
369
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Should succeed and return "sent" status
|
|
373
|
+
assert response.status_code == 200
|
|
374
|
+
data = response.json()
|
|
375
|
+
assert data["status"] == "sent"
|
|
376
|
+
assert data["job_id"] == "test-job-123"
|
|
377
|
+
|
|
378
|
+
def test_skips_reminder_when_already_sent(self, mock_job):
|
|
379
|
+
"""Test that reminder is skipped if already sent."""
|
|
380
|
+
from backend.api.routes.internal import router as internal_router
|
|
381
|
+
from backend.api.dependencies import require_admin
|
|
382
|
+
|
|
383
|
+
# Create test app for internal router
|
|
384
|
+
# The router already has prefix="/internal", so we add /api prefix
|
|
385
|
+
test_app = FastAPI()
|
|
386
|
+
test_app.include_router(internal_router, prefix="/api")
|
|
387
|
+
test_app.dependency_overrides[require_admin] = get_mock_admin
|
|
388
|
+
|
|
389
|
+
mock_job.status = JobStatus.AWAITING_REVIEW.value # Use .value for string comparison
|
|
390
|
+
mock_job.state_data = {
|
|
391
|
+
"blocking_state_entered_at": "2024-01-01T00:00:00",
|
|
392
|
+
"blocking_action_type": "lyrics",
|
|
393
|
+
"reminder_sent": True, # Already sent
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
with patch('backend.api.routes.internal.JobManager') as mock_jm_class:
|
|
397
|
+
mock_jm = Mock()
|
|
398
|
+
mock_jm.get_job.return_value = mock_job
|
|
399
|
+
mock_jm_class.return_value = mock_jm
|
|
400
|
+
|
|
401
|
+
test_client = TestClient(test_app)
|
|
402
|
+
response = test_client.post(
|
|
403
|
+
"/api/internal/jobs/test-job-123/check-idle-reminder",
|
|
404
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Should succeed with "already_sent" status
|
|
408
|
+
assert response.status_code == 200
|
|
409
|
+
data = response.json()
|
|
410
|
+
assert data["status"] == "already_sent"
|
|
411
|
+
assert data["job_id"] == "test-job-123"
|