karaoke-gen 0.99.3__py3-none-any.whl → 0.103.1__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/api/routes/admin.py +512 -1
- backend/api/routes/audio_search.py +17 -34
- backend/api/routes/file_upload.py +60 -84
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +11 -3
- backend/api/routes/rate_limits.py +428 -0
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +229 -247
- backend/config.py +16 -0
- backend/exceptions.py +66 -0
- backend/main.py +30 -1
- backend/middleware/__init__.py +7 -1
- backend/middleware/tenant.py +192 -0
- backend/models/job.py +19 -3
- backend/models/tenant.py +208 -0
- backend/models/user.py +18 -0
- backend/services/email_service.py +253 -6
- backend/services/email_validation_service.py +646 -0
- backend/services/firestore_service.py +27 -0
- backend/services/job_defaults_service.py +113 -0
- backend/services/job_manager.py +73 -3
- backend/services/rate_limit_service.py +641 -0
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/conftest.py +7 -1
- backend/tests/emulator/test_made_for_you_integration.py +167 -0
- backend/tests/test_admin_job_files.py +337 -0
- backend/tests/test_admin_job_reset.py +384 -0
- backend/tests/test_admin_job_update.py +326 -0
- backend/tests/test_audio_search.py +12 -8
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_email_validation_service.py +298 -0
- backend/tests/test_file_upload.py +8 -6
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +146 -1
- backend/tests/test_made_for_you.py +2088 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_rate_limit_service.py +396 -0
- backend/tests/test_rate_limits_api.py +392 -0
- backend/tests/test_tenant_api.py +350 -0
- backend/tests/test_tenant_middleware.py +345 -0
- backend/tests/test_tenant_models.py +406 -0
- backend/tests/test_tenant_service.py +418 -0
- backend/workers/video_worker.py +8 -3
- backend/workers/video_worker_orchestrator.py +26 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/RECORD +55 -33
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for admin job update endpoint.
|
|
3
|
+
|
|
4
|
+
Tests the PATCH /api/admin/jobs/{job_id} endpoint that allows admins
|
|
5
|
+
to update editable job fields.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
|
|
12
|
+
from backend.api.routes.admin import router
|
|
13
|
+
from backend.api.dependencies import require_admin
|
|
14
|
+
from backend.models.job import Job, JobStatus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Create a test app with the admin router
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
app.include_router(router, prefix="/api")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_mock_admin():
|
|
23
|
+
"""Override for require_admin dependency."""
|
|
24
|
+
from backend.api.dependencies import AuthResult, UserType
|
|
25
|
+
return AuthResult(
|
|
26
|
+
is_valid=True,
|
|
27
|
+
user_type=UserType.ADMIN,
|
|
28
|
+
remaining_uses=999,
|
|
29
|
+
message="Admin authenticated",
|
|
30
|
+
user_email="admin@example.com",
|
|
31
|
+
is_admin=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Override the require_admin dependency
|
|
36
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def client():
|
|
41
|
+
"""Create a test client."""
|
|
42
|
+
return TestClient(app)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def mock_job():
|
|
47
|
+
"""Create a mock job for testing."""
|
|
48
|
+
job = Mock(spec=Job)
|
|
49
|
+
job.job_id = "test-job-123"
|
|
50
|
+
job.user_email = "user@example.com"
|
|
51
|
+
job.artist = "Original Artist"
|
|
52
|
+
job.title = "Original Title"
|
|
53
|
+
job.status = JobStatus.AWAITING_REVIEW
|
|
54
|
+
job.theme_id = "nomad"
|
|
55
|
+
job.enable_cdg = False
|
|
56
|
+
job.enable_txt = False
|
|
57
|
+
job.enable_youtube_upload = True
|
|
58
|
+
job.customer_email = None
|
|
59
|
+
job.customer_notes = None
|
|
60
|
+
job.brand_prefix = None
|
|
61
|
+
job.non_interactive = False
|
|
62
|
+
job.prep_only = False
|
|
63
|
+
job.discord_webhook_url = None
|
|
64
|
+
job.youtube_description = None
|
|
65
|
+
job.youtube_description_template = None
|
|
66
|
+
job.file_urls = {}
|
|
67
|
+
job.state_data = {}
|
|
68
|
+
job.created_at = "2026-01-09T10:00:00Z"
|
|
69
|
+
job.updated_at = "2026-01-09T10:30:00Z"
|
|
70
|
+
return job
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestUpdateJob:
|
|
74
|
+
"""Tests for PATCH /api/admin/jobs/{job_id} endpoint."""
|
|
75
|
+
|
|
76
|
+
def test_update_artist_field(self, client, mock_job):
|
|
77
|
+
"""Test updating the artist field."""
|
|
78
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
79
|
+
mock_jm = Mock()
|
|
80
|
+
mock_jm.get_job.return_value = mock_job
|
|
81
|
+
mock_jm.update_job.return_value = True
|
|
82
|
+
mock_jm_class.return_value = mock_jm
|
|
83
|
+
|
|
84
|
+
response = client.patch(
|
|
85
|
+
"/api/admin/jobs/test-job-123",
|
|
86
|
+
json={"artist": "New Artist"},
|
|
87
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert response.status_code == 200
|
|
91
|
+
data = response.json()
|
|
92
|
+
assert data["status"] == "success"
|
|
93
|
+
assert data["job_id"] == "test-job-123"
|
|
94
|
+
assert "artist" in data["updated_fields"]
|
|
95
|
+
|
|
96
|
+
# Verify update_job was called with correct args
|
|
97
|
+
mock_jm.update_job.assert_called_once()
|
|
98
|
+
call_args = mock_jm.update_job.call_args
|
|
99
|
+
assert call_args[0][0] == "test-job-123"
|
|
100
|
+
assert call_args[0][1]["artist"] == "New Artist"
|
|
101
|
+
|
|
102
|
+
def test_update_multiple_fields(self, client, mock_job):
|
|
103
|
+
"""Test updating multiple fields at once."""
|
|
104
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
105
|
+
mock_jm = Mock()
|
|
106
|
+
mock_jm.get_job.return_value = mock_job
|
|
107
|
+
mock_jm.update_job.return_value = True
|
|
108
|
+
mock_jm_class.return_value = mock_jm
|
|
109
|
+
|
|
110
|
+
response = client.patch(
|
|
111
|
+
"/api/admin/jobs/test-job-123",
|
|
112
|
+
json={
|
|
113
|
+
"artist": "New Artist",
|
|
114
|
+
"title": "New Title",
|
|
115
|
+
"theme_id": "default",
|
|
116
|
+
"enable_cdg": True,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert response.status_code == 200
|
|
121
|
+
data = response.json()
|
|
122
|
+
assert len(data["updated_fields"]) == 4
|
|
123
|
+
assert "artist" in data["updated_fields"]
|
|
124
|
+
assert "title" in data["updated_fields"]
|
|
125
|
+
assert "theme_id" in data["updated_fields"]
|
|
126
|
+
assert "enable_cdg" in data["updated_fields"]
|
|
127
|
+
|
|
128
|
+
def test_update_user_email(self, client, mock_job):
|
|
129
|
+
"""Test updating user_email field."""
|
|
130
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
131
|
+
mock_jm = Mock()
|
|
132
|
+
mock_jm.get_job.return_value = mock_job
|
|
133
|
+
mock_jm.update_job.return_value = True
|
|
134
|
+
mock_jm_class.return_value = mock_jm
|
|
135
|
+
|
|
136
|
+
response = client.patch(
|
|
137
|
+
"/api/admin/jobs/test-job-123",
|
|
138
|
+
json={"user_email": "newuser@example.com"},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert response.status_code == 200
|
|
142
|
+
call_args = mock_jm.update_job.call_args
|
|
143
|
+
assert call_args[0][1]["user_email"] == "newuser@example.com"
|
|
144
|
+
|
|
145
|
+
def test_update_boolean_fields(self, client, mock_job):
|
|
146
|
+
"""Test updating boolean fields."""
|
|
147
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
148
|
+
mock_jm = Mock()
|
|
149
|
+
mock_jm.get_job.return_value = mock_job
|
|
150
|
+
mock_jm.update_job.return_value = True
|
|
151
|
+
mock_jm_class.return_value = mock_jm
|
|
152
|
+
|
|
153
|
+
response = client.patch(
|
|
154
|
+
"/api/admin/jobs/test-job-123",
|
|
155
|
+
json={
|
|
156
|
+
"enable_cdg": True,
|
|
157
|
+
"enable_txt": True,
|
|
158
|
+
"enable_youtube_upload": False,
|
|
159
|
+
"non_interactive": True,
|
|
160
|
+
"prep_only": True,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert response.status_code == 200
|
|
165
|
+
call_args = mock_jm.update_job.call_args
|
|
166
|
+
assert call_args[0][1]["enable_cdg"] is True
|
|
167
|
+
assert call_args[0][1]["enable_txt"] is True
|
|
168
|
+
assert call_args[0][1]["enable_youtube_upload"] is False
|
|
169
|
+
|
|
170
|
+
def test_rejects_non_editable_job_id(self, client, mock_job):
|
|
171
|
+
"""Test that job_id cannot be updated."""
|
|
172
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
173
|
+
mock_jm = Mock()
|
|
174
|
+
mock_jm.get_job.return_value = mock_job
|
|
175
|
+
mock_jm_class.return_value = mock_jm
|
|
176
|
+
|
|
177
|
+
response = client.patch(
|
|
178
|
+
"/api/admin/jobs/test-job-123",
|
|
179
|
+
json={"job_id": "new-job-id"},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
assert response.status_code == 400
|
|
183
|
+
assert "not editable" in response.json()["detail"].lower()
|
|
184
|
+
|
|
185
|
+
def test_rejects_non_editable_created_at(self, client, mock_job):
|
|
186
|
+
"""Test that created_at cannot be updated."""
|
|
187
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
188
|
+
mock_jm = Mock()
|
|
189
|
+
mock_jm.get_job.return_value = mock_job
|
|
190
|
+
mock_jm_class.return_value = mock_jm
|
|
191
|
+
|
|
192
|
+
response = client.patch(
|
|
193
|
+
"/api/admin/jobs/test-job-123",
|
|
194
|
+
json={"created_at": "2025-01-01T00:00:00Z"},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert response.status_code == 400
|
|
198
|
+
assert "not editable" in response.json()["detail"].lower()
|
|
199
|
+
|
|
200
|
+
def test_rejects_status_update(self, client, mock_job):
|
|
201
|
+
"""Test that status cannot be updated via PATCH (use reset endpoint)."""
|
|
202
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
203
|
+
mock_jm = Mock()
|
|
204
|
+
mock_jm.get_job.return_value = mock_job
|
|
205
|
+
mock_jm_class.return_value = mock_jm
|
|
206
|
+
|
|
207
|
+
response = client.patch(
|
|
208
|
+
"/api/admin/jobs/test-job-123",
|
|
209
|
+
json={"status": "complete"},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
assert response.status_code == 400
|
|
213
|
+
assert "not editable" in response.json()["detail"].lower()
|
|
214
|
+
|
|
215
|
+
def test_rejects_state_data_update(self, client, mock_job):
|
|
216
|
+
"""Test that state_data cannot be updated directly."""
|
|
217
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
218
|
+
mock_jm = Mock()
|
|
219
|
+
mock_jm.get_job.return_value = mock_job
|
|
220
|
+
mock_jm_class.return_value = mock_jm
|
|
221
|
+
|
|
222
|
+
response = client.patch(
|
|
223
|
+
"/api/admin/jobs/test-job-123",
|
|
224
|
+
json={"state_data": {"foo": "bar"}},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
assert response.status_code == 400
|
|
228
|
+
assert "not editable" in response.json()["detail"].lower()
|
|
229
|
+
|
|
230
|
+
def test_returns_404_when_job_not_found(self, client):
|
|
231
|
+
"""Test 404 when job doesn't exist."""
|
|
232
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
233
|
+
mock_jm = Mock()
|
|
234
|
+
mock_jm.get_job.return_value = None
|
|
235
|
+
mock_jm_class.return_value = mock_jm
|
|
236
|
+
|
|
237
|
+
response = client.patch(
|
|
238
|
+
"/api/admin/jobs/nonexistent-job",
|
|
239
|
+
json={"artist": "New Artist"},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert response.status_code == 404
|
|
243
|
+
assert "not found" in response.json()["detail"].lower()
|
|
244
|
+
|
|
245
|
+
def test_returns_400_for_empty_update(self, client, mock_job):
|
|
246
|
+
"""Test 400 when no valid fields are provided."""
|
|
247
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
248
|
+
mock_jm = Mock()
|
|
249
|
+
mock_jm.get_job.return_value = mock_job
|
|
250
|
+
mock_jm_class.return_value = mock_jm
|
|
251
|
+
|
|
252
|
+
response = client.patch(
|
|
253
|
+
"/api/admin/jobs/test-job-123",
|
|
254
|
+
json={},
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
assert response.status_code == 400
|
|
258
|
+
assert "no valid" in response.json()["detail"].lower()
|
|
259
|
+
|
|
260
|
+
def test_logs_admin_changes(self, client, mock_job):
|
|
261
|
+
"""Test that admin changes are logged."""
|
|
262
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
263
|
+
patch('backend.api.routes.admin.logger') as mock_logger:
|
|
264
|
+
mock_jm = Mock()
|
|
265
|
+
mock_jm.get_job.return_value = mock_job
|
|
266
|
+
mock_jm.update_job.return_value = True
|
|
267
|
+
mock_jm_class.return_value = mock_jm
|
|
268
|
+
|
|
269
|
+
response = client.patch(
|
|
270
|
+
"/api/admin/jobs/test-job-123",
|
|
271
|
+
json={"artist": "New Artist"},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
assert response.status_code == 200
|
|
275
|
+
# Verify logging was called
|
|
276
|
+
mock_logger.info.assert_called()
|
|
277
|
+
log_message = mock_logger.info.call_args[0][0]
|
|
278
|
+
assert "admin@example.com" in log_message.lower() or "admin" in log_message.lower()
|
|
279
|
+
|
|
280
|
+
def test_update_customer_fields(self, client, mock_job):
|
|
281
|
+
"""Test updating made-for-you customer fields."""
|
|
282
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
283
|
+
mock_jm = Mock()
|
|
284
|
+
mock_jm.get_job.return_value = mock_job
|
|
285
|
+
mock_jm.update_job.return_value = True
|
|
286
|
+
mock_jm_class.return_value = mock_jm
|
|
287
|
+
|
|
288
|
+
response = client.patch(
|
|
289
|
+
"/api/admin/jobs/test-job-123",
|
|
290
|
+
json={
|
|
291
|
+
"customer_email": "customer@example.com",
|
|
292
|
+
"customer_notes": "Special request: add extra countdown",
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
assert response.status_code == 200
|
|
297
|
+
call_args = mock_jm.update_job.call_args
|
|
298
|
+
assert call_args[0][1]["customer_email"] == "customer@example.com"
|
|
299
|
+
assert call_args[0][1]["customer_notes"] == "Special request: add extra countdown"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestUpdateJobAuthorization:
|
|
303
|
+
"""Tests for authorization on the update endpoint."""
|
|
304
|
+
|
|
305
|
+
def test_requires_admin_access(self, client, mock_job):
|
|
306
|
+
"""Test that non-admin users cannot access the endpoint."""
|
|
307
|
+
original_override = app.dependency_overrides.get(require_admin)
|
|
308
|
+
|
|
309
|
+
def get_non_admin():
|
|
310
|
+
from fastapi import HTTPException
|
|
311
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
312
|
+
|
|
313
|
+
app.dependency_overrides[require_admin] = get_non_admin
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
response = client.patch(
|
|
317
|
+
"/api/admin/jobs/test-job-123",
|
|
318
|
+
json={"artist": "New Artist"},
|
|
319
|
+
headers={"Authorization": "Bearer user-token"}
|
|
320
|
+
)
|
|
321
|
+
assert response.status_code == 403
|
|
322
|
+
finally:
|
|
323
|
+
if original_override:
|
|
324
|
+
app.dependency_overrides[require_admin] = original_override
|
|
325
|
+
else:
|
|
326
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
@@ -845,7 +845,8 @@ class TestAudioSearchThemeSupport:
|
|
|
845
845
|
This is the key behavior: selecting a theme should automatically
|
|
846
846
|
enable CDG and TXT output formats.
|
|
847
847
|
"""
|
|
848
|
-
from backend.api.routes.audio_search import AudioSearchRequest
|
|
848
|
+
from backend.api.routes.audio_search import AudioSearchRequest
|
|
849
|
+
from backend.services.job_defaults_service import resolve_cdg_txt_defaults
|
|
849
850
|
|
|
850
851
|
# When theme_id is set, enable_cdg/enable_txt should default to True
|
|
851
852
|
request = AudioSearchRequest(
|
|
@@ -855,7 +856,7 @@ class TestAudioSearchThemeSupport:
|
|
|
855
856
|
# enable_cdg and enable_txt are None (not specified)
|
|
856
857
|
)
|
|
857
858
|
|
|
858
|
-
resolved_cdg, resolved_txt =
|
|
859
|
+
resolved_cdg, resolved_txt = resolve_cdg_txt_defaults(
|
|
859
860
|
request.theme_id, request.enable_cdg, request.enable_txt
|
|
860
861
|
)
|
|
861
862
|
|
|
@@ -864,7 +865,8 @@ class TestAudioSearchThemeSupport:
|
|
|
864
865
|
|
|
865
866
|
def test_audio_search_request_no_theme_no_cdg_txt(self):
|
|
866
867
|
"""Test that without theme_id, CDG/TXT defaults to disabled."""
|
|
867
|
-
from backend.api.routes.audio_search import AudioSearchRequest
|
|
868
|
+
from backend.api.routes.audio_search import AudioSearchRequest
|
|
869
|
+
from backend.services.job_defaults_service import resolve_cdg_txt_defaults
|
|
868
870
|
|
|
869
871
|
request = AudioSearchRequest(
|
|
870
872
|
artist="Test Artist",
|
|
@@ -872,7 +874,7 @@ class TestAudioSearchThemeSupport:
|
|
|
872
874
|
# No theme_id, no enable_cdg, no enable_txt
|
|
873
875
|
)
|
|
874
876
|
|
|
875
|
-
resolved_cdg, resolved_txt =
|
|
877
|
+
resolved_cdg, resolved_txt = resolve_cdg_txt_defaults(
|
|
876
878
|
request.theme_id, request.enable_cdg, request.enable_txt
|
|
877
879
|
)
|
|
878
880
|
|
|
@@ -881,7 +883,8 @@ class TestAudioSearchThemeSupport:
|
|
|
881
883
|
|
|
882
884
|
def test_explicit_cdg_txt_overrides_theme_default(self):
|
|
883
885
|
"""Test that explicit enable_cdg/enable_txt values override theme defaults."""
|
|
884
|
-
from backend.api.routes.audio_search import AudioSearchRequest
|
|
886
|
+
from backend.api.routes.audio_search import AudioSearchRequest
|
|
887
|
+
from backend.services.job_defaults_service import resolve_cdg_txt_defaults
|
|
885
888
|
|
|
886
889
|
# Theme set (would default to True), but explicitly disabled
|
|
887
890
|
request = AudioSearchRequest(
|
|
@@ -892,7 +895,7 @@ class TestAudioSearchThemeSupport:
|
|
|
892
895
|
enable_txt=False,
|
|
893
896
|
)
|
|
894
897
|
|
|
895
|
-
resolved_cdg, resolved_txt =
|
|
898
|
+
resolved_cdg, resolved_txt = resolve_cdg_txt_defaults(
|
|
896
899
|
request.theme_id, request.enable_cdg, request.enable_txt
|
|
897
900
|
)
|
|
898
901
|
|
|
@@ -901,7 +904,8 @@ class TestAudioSearchThemeSupport:
|
|
|
901
904
|
|
|
902
905
|
def test_explicit_cdg_txt_enables_without_theme(self):
|
|
903
906
|
"""Test that explicit True enables CDG/TXT even without theme."""
|
|
904
|
-
from backend.api.routes.audio_search import AudioSearchRequest
|
|
907
|
+
from backend.api.routes.audio_search import AudioSearchRequest
|
|
908
|
+
from backend.services.job_defaults_service import resolve_cdg_txt_defaults
|
|
905
909
|
|
|
906
910
|
# No theme (would default to False), but explicitly enabled
|
|
907
911
|
request = AudioSearchRequest(
|
|
@@ -911,7 +915,7 @@ class TestAudioSearchThemeSupport:
|
|
|
911
915
|
enable_txt=True,
|
|
912
916
|
)
|
|
913
917
|
|
|
914
|
-
resolved_cdg, resolved_txt =
|
|
918
|
+
resolved_cdg, resolved_txt = resolve_cdg_txt_defaults(
|
|
915
919
|
request.theme_id, request.enable_cdg, request.enable_txt
|
|
916
920
|
)
|
|
917
921
|
|
|
@@ -490,3 +490,236 @@ class TestCCFunctionality:
|
|
|
490
490
|
|
|
491
491
|
# Verify add_cc was called
|
|
492
492
|
mock_mail.add_cc.assert_called()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TestMadeForYouOrderConfirmation:
|
|
496
|
+
"""Tests for send_made_for_you_order_confirmation method."""
|
|
497
|
+
|
|
498
|
+
def test_send_order_confirmation_success(self):
|
|
499
|
+
"""Test successful order confirmation email."""
|
|
500
|
+
service = EmailService()
|
|
501
|
+
service.provider = Mock()
|
|
502
|
+
service.provider.send_email.return_value = True
|
|
503
|
+
|
|
504
|
+
result = service.send_made_for_you_order_confirmation(
|
|
505
|
+
to_email="customer@example.com",
|
|
506
|
+
artist="Test Artist",
|
|
507
|
+
title="Test Song",
|
|
508
|
+
job_id="test-job-123",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
assert result is True
|
|
512
|
+
service.provider.send_email.assert_called_once()
|
|
513
|
+
# Verify BCC is set
|
|
514
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
515
|
+
assert call_kwargs.get('bcc_emails') == ["done@nomadkaraoke.com"]
|
|
516
|
+
|
|
517
|
+
def test_send_order_confirmation_subject_format(self):
|
|
518
|
+
"""Test order confirmation has correct subject format."""
|
|
519
|
+
service = EmailService()
|
|
520
|
+
service.provider = Mock()
|
|
521
|
+
service.provider.send_email.return_value = True
|
|
522
|
+
|
|
523
|
+
service.send_made_for_you_order_confirmation(
|
|
524
|
+
to_email="customer@example.com",
|
|
525
|
+
artist="Seether",
|
|
526
|
+
title="Tonight",
|
|
527
|
+
job_id="test-job-456",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
531
|
+
subject = call_kwargs.get('subject')
|
|
532
|
+
assert "Order Confirmed" in subject
|
|
533
|
+
assert "Seether" in subject
|
|
534
|
+
assert "Tonight" in subject
|
|
535
|
+
|
|
536
|
+
def test_send_order_confirmation_with_notes(self):
|
|
537
|
+
"""Test order confirmation includes notes."""
|
|
538
|
+
service = EmailService()
|
|
539
|
+
service.provider = Mock()
|
|
540
|
+
service.provider.send_email.return_value = True
|
|
541
|
+
|
|
542
|
+
service.send_made_for_you_order_confirmation(
|
|
543
|
+
to_email="customer@example.com",
|
|
544
|
+
artist="Test Artist",
|
|
545
|
+
title="Test Song",
|
|
546
|
+
job_id="test-job-789",
|
|
547
|
+
notes="Wedding anniversary!",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
551
|
+
html_content = call_kwargs.get('html_content')
|
|
552
|
+
assert "Wedding anniversary!" in html_content
|
|
553
|
+
|
|
554
|
+
def test_send_order_confirmation_includes_delivery_promise(self):
|
|
555
|
+
"""Test order confirmation mentions delivery timeframe."""
|
|
556
|
+
service = EmailService()
|
|
557
|
+
service.provider = Mock()
|
|
558
|
+
service.provider.send_email.return_value = True
|
|
559
|
+
|
|
560
|
+
service.send_made_for_you_order_confirmation(
|
|
561
|
+
to_email="customer@example.com",
|
|
562
|
+
artist="Test Artist",
|
|
563
|
+
title="Test Song",
|
|
564
|
+
job_id="test-job-abc",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
568
|
+
html_content = call_kwargs.get('html_content', '')
|
|
569
|
+
text_content = call_kwargs.get('text_content', '')
|
|
570
|
+
# Should mention delivery timeline
|
|
571
|
+
assert "24" in html_content or "24" in text_content
|
|
572
|
+
|
|
573
|
+
def test_send_order_confirmation_failure(self):
|
|
574
|
+
"""Test handling of email send failure."""
|
|
575
|
+
service = EmailService()
|
|
576
|
+
service.provider = Mock()
|
|
577
|
+
service.provider.send_email.return_value = False
|
|
578
|
+
|
|
579
|
+
result = service.send_made_for_you_order_confirmation(
|
|
580
|
+
to_email="customer@example.com",
|
|
581
|
+
artist="Test Artist",
|
|
582
|
+
title="Test Song",
|
|
583
|
+
job_id="test-job-def",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
assert result is False
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class TestMadeForYouAdminNotification:
|
|
590
|
+
"""Tests for send_made_for_you_admin_notification method."""
|
|
591
|
+
|
|
592
|
+
def test_send_admin_notification_success(self):
|
|
593
|
+
"""Test successful admin notification email."""
|
|
594
|
+
service = EmailService()
|
|
595
|
+
service.provider = Mock()
|
|
596
|
+
service.provider.send_email.return_value = True
|
|
597
|
+
|
|
598
|
+
result = service.send_made_for_you_admin_notification(
|
|
599
|
+
to_email="admin@nomadkaraoke.com",
|
|
600
|
+
customer_email="customer@example.com",
|
|
601
|
+
artist="Test Artist",
|
|
602
|
+
title="Test Song",
|
|
603
|
+
job_id="job-123",
|
|
604
|
+
admin_login_token="test-token-abc",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
assert result is True
|
|
608
|
+
service.provider.send_email.assert_called_once()
|
|
609
|
+
|
|
610
|
+
def test_send_admin_notification_subject_format(self):
|
|
611
|
+
"""Test admin notification has correct subject format."""
|
|
612
|
+
service = EmailService()
|
|
613
|
+
service.provider = Mock()
|
|
614
|
+
service.provider.send_email.return_value = True
|
|
615
|
+
|
|
616
|
+
service.send_made_for_you_admin_notification(
|
|
617
|
+
to_email="admin@nomadkaraoke.com",
|
|
618
|
+
customer_email="customer@example.com",
|
|
619
|
+
artist="Seether",
|
|
620
|
+
title="Tonight",
|
|
621
|
+
job_id="job-123",
|
|
622
|
+
admin_login_token="test-token-abc",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
626
|
+
subject = call_kwargs.get('subject')
|
|
627
|
+
# Subject format: "Karaoke Order: {artist} - {title} [ID: {job_id}]"
|
|
628
|
+
assert "Karaoke Order" in subject
|
|
629
|
+
assert "Seether" in subject
|
|
630
|
+
assert "Tonight" in subject
|
|
631
|
+
|
|
632
|
+
def test_send_admin_notification_includes_customer_email(self):
|
|
633
|
+
"""Test admin notification includes customer email."""
|
|
634
|
+
service = EmailService()
|
|
635
|
+
service.provider = Mock()
|
|
636
|
+
service.provider.send_email.return_value = True
|
|
637
|
+
|
|
638
|
+
service.send_made_for_you_admin_notification(
|
|
639
|
+
to_email="admin@nomadkaraoke.com",
|
|
640
|
+
customer_email="vip@example.com",
|
|
641
|
+
artist="Test Artist",
|
|
642
|
+
title="Test Song",
|
|
643
|
+
job_id="job-123",
|
|
644
|
+
admin_login_token="test-token-abc",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
648
|
+
html_content = call_kwargs.get('html_content')
|
|
649
|
+
assert "vip@example.com" in html_content
|
|
650
|
+
|
|
651
|
+
def test_send_admin_notification_includes_job_link(self):
|
|
652
|
+
"""Test admin notification includes job link."""
|
|
653
|
+
service = EmailService()
|
|
654
|
+
service.provider = Mock()
|
|
655
|
+
service.provider.send_email.return_value = True
|
|
656
|
+
|
|
657
|
+
service.send_made_for_you_admin_notification(
|
|
658
|
+
to_email="admin@nomadkaraoke.com",
|
|
659
|
+
customer_email="customer@example.com",
|
|
660
|
+
artist="Test Artist",
|
|
661
|
+
title="Test Song",
|
|
662
|
+
job_id="abc-def-123",
|
|
663
|
+
admin_login_token="test-token-abc",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
667
|
+
html_content = call_kwargs.get('html_content')
|
|
668
|
+
assert "abc-def-123" in html_content
|
|
669
|
+
|
|
670
|
+
def test_send_admin_notification_includes_audio_count(self):
|
|
671
|
+
"""Test admin notification includes audio source count."""
|
|
672
|
+
service = EmailService()
|
|
673
|
+
service.provider = Mock()
|
|
674
|
+
service.provider.send_email.return_value = True
|
|
675
|
+
|
|
676
|
+
service.send_made_for_you_admin_notification(
|
|
677
|
+
to_email="admin@nomadkaraoke.com",
|
|
678
|
+
customer_email="customer@example.com",
|
|
679
|
+
artist="Test Artist",
|
|
680
|
+
title="Test Song",
|
|
681
|
+
job_id="job-123",
|
|
682
|
+
admin_login_token="test-token-abc",
|
|
683
|
+
audio_source_count=5,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
687
|
+
html_content = call_kwargs.get('html_content')
|
|
688
|
+
assert "5" in html_content
|
|
689
|
+
|
|
690
|
+
def test_send_admin_notification_includes_notes(self):
|
|
691
|
+
"""Test admin notification includes customer notes."""
|
|
692
|
+
service = EmailService()
|
|
693
|
+
service.provider = Mock()
|
|
694
|
+
service.provider.send_email.return_value = True
|
|
695
|
+
|
|
696
|
+
service.send_made_for_you_admin_notification(
|
|
697
|
+
to_email="admin@nomadkaraoke.com",
|
|
698
|
+
customer_email="customer@example.com",
|
|
699
|
+
artist="Test Artist",
|
|
700
|
+
title="Test Song",
|
|
701
|
+
job_id="job-123",
|
|
702
|
+
admin_login_token="test-token-abc",
|
|
703
|
+
notes="Rush order please!",
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
call_kwargs = service.provider.send_email.call_args.kwargs
|
|
707
|
+
html_content = call_kwargs.get('html_content')
|
|
708
|
+
assert "Rush order please!" in html_content
|
|
709
|
+
|
|
710
|
+
def test_send_admin_notification_failure(self):
|
|
711
|
+
"""Test handling of email send failure."""
|
|
712
|
+
service = EmailService()
|
|
713
|
+
service.provider = Mock()
|
|
714
|
+
service.provider.send_email.return_value = False
|
|
715
|
+
|
|
716
|
+
result = service.send_made_for_you_admin_notification(
|
|
717
|
+
to_email="admin@nomadkaraoke.com",
|
|
718
|
+
customer_email="customer@example.com",
|
|
719
|
+
artist="Test Artist",
|
|
720
|
+
title="Test Song",
|
|
721
|
+
job_id="job-123",
|
|
722
|
+
admin_login_token="test-token-abc",
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
assert result is False
|