karaoke-gen 0.96.0__py3-none-any.whl → 0.101.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/api/routes/admin.py +696 -92
- backend/api/routes/audio_search.py +29 -8
- backend/api/routes/file_upload.py +99 -22
- backend/api/routes/health.py +65 -0
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +28 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +472 -51
- backend/main.py +31 -2
- 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/encoding_service.py +128 -31
- backend/services/firestore_service.py +6 -0
- backend/services/job_manager.py +44 -2
- backend/services/langfuse_preloader.py +98 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/stripe_service.py +133 -11
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/emulator/conftest.py +22 -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_email_service.py +233 -0
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +171 -9
- backend/tests/test_jobs_api.py +11 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_spacy_preloader.py +119 -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/utils/test_data.py +27 -0
- backend/workers/screens_worker.py +16 -6
- backend/workers/video_worker.py +8 -3
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +58 -39
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -43
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.96.0.dist-info → karaoke_gen-0.101.0.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
|
|
@@ -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
|