karaoke-gen 0.101.0__py3-none-any.whl → 0.105.4__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/Dockerfile.base +1 -0
- backend/api/routes/admin.py +226 -3
- backend/api/routes/audio_search.py +4 -32
- backend/api/routes/file_upload.py +18 -83
- backend/api/routes/jobs.py +2 -2
- backend/api/routes/push.py +238 -0
- backend/api/routes/rate_limits.py +428 -0
- backend/api/routes/users.py +79 -19
- backend/config.py +25 -1
- backend/exceptions.py +66 -0
- backend/main.py +26 -1
- backend/models/job.py +4 -0
- backend/models/user.py +20 -2
- backend/services/email_validation_service.py +646 -0
- backend/services/firestore_service.py +21 -0
- backend/services/gce_encoding/main.py +22 -8
- backend/services/job_defaults_service.py +113 -0
- backend/services/job_manager.py +109 -13
- backend/services/push_notification_service.py +409 -0
- backend/services/rate_limit_service.py +641 -0
- backend/services/stripe_service.py +2 -2
- backend/tests/conftest.py +8 -1
- backend/tests/test_admin_delete_outputs.py +352 -0
- backend/tests/test_audio_search.py +12 -8
- backend/tests/test_email_validation_service.py +298 -0
- backend/tests/test_file_upload.py +8 -6
- backend/tests/test_gce_encoding_worker.py +229 -0
- backend/tests/test_impersonation.py +18 -3
- backend/tests/test_made_for_you.py +6 -4
- backend/tests/test_push_notification_service.py +460 -0
- backend/tests/test_push_routes.py +357 -0
- backend/tests/test_rate_limit_service.py +396 -0
- backend/tests/test_rate_limits_api.py +392 -0
- backend/tests/test_stripe_service.py +205 -0
- backend/workers/video_worker_orchestrator.py +42 -0
- karaoke_gen/instrumental_review/static/index.html +35 -9
- {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/METADATA +2 -1
- {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/RECORD +41 -26
- {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for admin delete job outputs endpoint.
|
|
3
|
+
|
|
4
|
+
Tests the POST /api/admin/jobs/{job_id}/delete-outputs endpoint that allows admins
|
|
5
|
+
to delete distributed outputs (YouTube, Dropbox, Google Drive) while preserving
|
|
6
|
+
the job record.
|
|
7
|
+
"""
|
|
8
|
+
import pytest
|
|
9
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from backend.api.routes.admin import router
|
|
15
|
+
from backend.api.dependencies import require_admin
|
|
16
|
+
from backend.models.job import Job, JobStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Create a test app with the admin router
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
app.include_router(router, prefix="/api")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_mock_admin():
|
|
25
|
+
"""Override for require_admin dependency."""
|
|
26
|
+
from backend.api.dependencies import AuthResult, UserType
|
|
27
|
+
return AuthResult(
|
|
28
|
+
is_valid=True,
|
|
29
|
+
user_type=UserType.ADMIN,
|
|
30
|
+
remaining_uses=999,
|
|
31
|
+
message="Admin authenticated",
|
|
32
|
+
user_email="admin@example.com",
|
|
33
|
+
is_admin=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Override the require_admin dependency
|
|
38
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def client():
|
|
43
|
+
"""Create a test client."""
|
|
44
|
+
return TestClient(app)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def mock_complete_job():
|
|
49
|
+
"""Create a mock job in COMPLETE status with distribution data."""
|
|
50
|
+
job = Mock(spec=Job)
|
|
51
|
+
job.job_id = "test-job-123"
|
|
52
|
+
job.user_email = "user@example.com"
|
|
53
|
+
job.artist = "Test Artist"
|
|
54
|
+
job.title = "Test Title"
|
|
55
|
+
job.status = "complete"
|
|
56
|
+
job.dropbox_path = "/Karaoke/Organized"
|
|
57
|
+
job.outputs_deleted_at = None
|
|
58
|
+
job.outputs_deleted_by = None
|
|
59
|
+
job.state_data = {
|
|
60
|
+
"youtube_url": "https://youtu.be/abc123",
|
|
61
|
+
"brand_code": "NOMAD-1234",
|
|
62
|
+
"dropbox_link": "https://dropbox.com/...",
|
|
63
|
+
"gdrive_files": {"mp4": "file_id_1", "mp4_720p": "file_id_2"},
|
|
64
|
+
}
|
|
65
|
+
job.timeline = []
|
|
66
|
+
return job
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def mock_job_no_outputs():
|
|
71
|
+
"""Create a mock job in COMPLETE status without distribution data."""
|
|
72
|
+
job = Mock(spec=Job)
|
|
73
|
+
job.job_id = "test-job-456"
|
|
74
|
+
job.user_email = "user@example.com"
|
|
75
|
+
job.artist = "Test Artist"
|
|
76
|
+
job.title = "Test Title"
|
|
77
|
+
job.status = "complete"
|
|
78
|
+
job.dropbox_path = None
|
|
79
|
+
job.outputs_deleted_at = None
|
|
80
|
+
job.outputs_deleted_by = None
|
|
81
|
+
job.state_data = {}
|
|
82
|
+
job.timeline = []
|
|
83
|
+
return job
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestDeleteOutputsSuccess:
|
|
87
|
+
"""Tests for successful output deletion."""
|
|
88
|
+
|
|
89
|
+
def test_delete_outputs_success(self, client, mock_complete_job):
|
|
90
|
+
"""Test successfully deleting outputs from a complete job."""
|
|
91
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
92
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
93
|
+
mock_jm = Mock()
|
|
94
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
95
|
+
mock_jm_class.return_value = mock_jm
|
|
96
|
+
|
|
97
|
+
# Mock Firestore
|
|
98
|
+
mock_db = Mock()
|
|
99
|
+
mock_job_ref = Mock()
|
|
100
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
101
|
+
mock_user_service.return_value.db = mock_db
|
|
102
|
+
|
|
103
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
104
|
+
|
|
105
|
+
assert response.status_code == 200
|
|
106
|
+
data = response.json()
|
|
107
|
+
assert data["status"] == "success"
|
|
108
|
+
assert data["job_id"] == "test-job-123"
|
|
109
|
+
assert "outputs_deleted_at" in data
|
|
110
|
+
|
|
111
|
+
def test_delete_outputs_clears_state_data_keys(self, client, mock_complete_job):
|
|
112
|
+
"""Test that output-related state_data keys are listed as cleared."""
|
|
113
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
114
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
115
|
+
mock_jm = Mock()
|
|
116
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
117
|
+
mock_jm_class.return_value = mock_jm
|
|
118
|
+
|
|
119
|
+
# Mock Firestore
|
|
120
|
+
mock_db = Mock()
|
|
121
|
+
mock_job_ref = Mock()
|
|
122
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
123
|
+
mock_user_service.return_value.db = mock_db
|
|
124
|
+
|
|
125
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
126
|
+
|
|
127
|
+
assert response.status_code == 200
|
|
128
|
+
data = response.json()
|
|
129
|
+
# Should list cleared keys
|
|
130
|
+
assert "youtube_url" in data["cleared_state_data"]
|
|
131
|
+
assert "brand_code" in data["cleared_state_data"]
|
|
132
|
+
assert "dropbox_link" in data["cleared_state_data"]
|
|
133
|
+
assert "gdrive_files" in data["cleared_state_data"]
|
|
134
|
+
|
|
135
|
+
def test_delete_outputs_job_without_distribution(self, client, mock_job_no_outputs):
|
|
136
|
+
"""Test deleting outputs from job that has no distribution data."""
|
|
137
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
138
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
139
|
+
mock_jm = Mock()
|
|
140
|
+
mock_jm.get_job.return_value = mock_job_no_outputs
|
|
141
|
+
mock_jm_class.return_value = mock_jm
|
|
142
|
+
|
|
143
|
+
# Mock Firestore
|
|
144
|
+
mock_db = Mock()
|
|
145
|
+
mock_job_ref = Mock()
|
|
146
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
147
|
+
mock_user_service.return_value.db = mock_db
|
|
148
|
+
|
|
149
|
+
response = client.post("/api/admin/jobs/test-job-456/delete-outputs")
|
|
150
|
+
|
|
151
|
+
assert response.status_code == 200
|
|
152
|
+
data = response.json()
|
|
153
|
+
assert data["status"] == "success"
|
|
154
|
+
# All services should be skipped
|
|
155
|
+
assert data["deleted_services"]["youtube"]["status"] == "skipped"
|
|
156
|
+
assert data["deleted_services"]["dropbox"]["status"] == "skipped"
|
|
157
|
+
assert data["deleted_services"]["gdrive"]["status"] == "skipped"
|
|
158
|
+
# No keys to clear
|
|
159
|
+
assert data["cleared_state_data"] == []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestDeleteOutputsValidation:
|
|
163
|
+
"""Tests for validation on delete outputs endpoint."""
|
|
164
|
+
|
|
165
|
+
def test_rejects_non_terminal_status(self, client, mock_complete_job):
|
|
166
|
+
"""Test that jobs not in terminal states are rejected."""
|
|
167
|
+
mock_complete_job.status = "encoding"
|
|
168
|
+
|
|
169
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
170
|
+
mock_jm = Mock()
|
|
171
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
172
|
+
mock_jm_class.return_value = mock_jm
|
|
173
|
+
|
|
174
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
175
|
+
|
|
176
|
+
assert response.status_code == 400
|
|
177
|
+
assert "terminal" in response.json()["detail"].lower()
|
|
178
|
+
|
|
179
|
+
def test_rejects_awaiting_review_status(self, client, mock_complete_job):
|
|
180
|
+
"""Test that AWAITING_REVIEW is rejected (not terminal)."""
|
|
181
|
+
mock_complete_job.status = "awaiting_review"
|
|
182
|
+
|
|
183
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
184
|
+
mock_jm = Mock()
|
|
185
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
186
|
+
mock_jm_class.return_value = mock_jm
|
|
187
|
+
|
|
188
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
189
|
+
|
|
190
|
+
assert response.status_code == 400
|
|
191
|
+
|
|
192
|
+
def test_accepts_prep_complete_status(self, client, mock_complete_job):
|
|
193
|
+
"""Test that prep_complete is accepted as terminal."""
|
|
194
|
+
mock_complete_job.status = "prep_complete"
|
|
195
|
+
|
|
196
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
197
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
198
|
+
mock_jm = Mock()
|
|
199
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
200
|
+
mock_jm_class.return_value = mock_jm
|
|
201
|
+
|
|
202
|
+
mock_db = Mock()
|
|
203
|
+
mock_job_ref = Mock()
|
|
204
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
205
|
+
mock_user_service.return_value.db = mock_db
|
|
206
|
+
|
|
207
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
208
|
+
|
|
209
|
+
assert response.status_code == 200
|
|
210
|
+
|
|
211
|
+
def test_accepts_failed_status(self, client, mock_complete_job):
|
|
212
|
+
"""Test that failed is accepted as terminal."""
|
|
213
|
+
mock_complete_job.status = "failed"
|
|
214
|
+
|
|
215
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
216
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
217
|
+
mock_jm = Mock()
|
|
218
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
219
|
+
mock_jm_class.return_value = mock_jm
|
|
220
|
+
|
|
221
|
+
mock_db = Mock()
|
|
222
|
+
mock_job_ref = Mock()
|
|
223
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
224
|
+
mock_user_service.return_value.db = mock_db
|
|
225
|
+
|
|
226
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
227
|
+
|
|
228
|
+
assert response.status_code == 200
|
|
229
|
+
|
|
230
|
+
def test_rejects_already_deleted(self, client, mock_complete_job):
|
|
231
|
+
"""Test that already-deleted outputs cannot be deleted again."""
|
|
232
|
+
mock_complete_job.outputs_deleted_at = datetime(2026, 1, 9, 12, 0, 0)
|
|
233
|
+
|
|
234
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
235
|
+
mock_jm = Mock()
|
|
236
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
237
|
+
mock_jm_class.return_value = mock_jm
|
|
238
|
+
|
|
239
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
240
|
+
|
|
241
|
+
assert response.status_code == 400
|
|
242
|
+
assert "already deleted" in response.json()["detail"].lower()
|
|
243
|
+
|
|
244
|
+
def test_returns_404_for_missing_job(self, client):
|
|
245
|
+
"""Test 404 when job doesn't exist."""
|
|
246
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
247
|
+
mock_jm = Mock()
|
|
248
|
+
mock_jm.get_job.return_value = None
|
|
249
|
+
mock_jm_class.return_value = mock_jm
|
|
250
|
+
|
|
251
|
+
response = client.post("/api/admin/jobs/nonexistent/delete-outputs")
|
|
252
|
+
|
|
253
|
+
assert response.status_code == 404
|
|
254
|
+
assert "not found" in response.json()["detail"].lower()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestDeleteOutputsServices:
|
|
258
|
+
"""Tests for service deletion behavior."""
|
|
259
|
+
|
|
260
|
+
def test_handles_missing_youtube(self, client, mock_complete_job):
|
|
261
|
+
"""Test handling when job has no YouTube URL."""
|
|
262
|
+
mock_complete_job.state_data = {"brand_code": "NOMAD-1234"}
|
|
263
|
+
|
|
264
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
265
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
266
|
+
mock_jm = Mock()
|
|
267
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
268
|
+
mock_jm_class.return_value = mock_jm
|
|
269
|
+
|
|
270
|
+
mock_db = Mock()
|
|
271
|
+
mock_job_ref = Mock()
|
|
272
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
273
|
+
mock_user_service.return_value.db = mock_db
|
|
274
|
+
|
|
275
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
276
|
+
|
|
277
|
+
assert response.status_code == 200
|
|
278
|
+
data = response.json()
|
|
279
|
+
assert data["deleted_services"]["youtube"]["status"] == "skipped"
|
|
280
|
+
|
|
281
|
+
def test_handles_missing_dropbox_path(self, client, mock_complete_job):
|
|
282
|
+
"""Test handling when job has no dropbox_path."""
|
|
283
|
+
mock_complete_job.dropbox_path = None
|
|
284
|
+
mock_complete_job.state_data = {"youtube_url": "https://youtu.be/abc123"}
|
|
285
|
+
|
|
286
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
287
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service:
|
|
288
|
+
mock_jm = Mock()
|
|
289
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
290
|
+
mock_jm_class.return_value = mock_jm
|
|
291
|
+
|
|
292
|
+
mock_db = Mock()
|
|
293
|
+
mock_job_ref = Mock()
|
|
294
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
295
|
+
mock_user_service.return_value.db = mock_db
|
|
296
|
+
|
|
297
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
298
|
+
|
|
299
|
+
assert response.status_code == 200
|
|
300
|
+
data = response.json()
|
|
301
|
+
assert data["deleted_services"]["dropbox"]["status"] == "skipped"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TestDeleteOutputsLogging:
|
|
305
|
+
"""Tests for logging on delete outputs endpoint."""
|
|
306
|
+
|
|
307
|
+
def test_logs_admin_action(self, client, mock_complete_job):
|
|
308
|
+
"""Test that admin delete action is logged."""
|
|
309
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
310
|
+
patch('backend.api.routes.admin.get_user_service') as mock_user_service, \
|
|
311
|
+
patch('backend.api.routes.admin.logger') as mock_logger:
|
|
312
|
+
mock_jm = Mock()
|
|
313
|
+
mock_jm.get_job.return_value = mock_complete_job
|
|
314
|
+
mock_jm_class.return_value = mock_jm
|
|
315
|
+
|
|
316
|
+
mock_db = Mock()
|
|
317
|
+
mock_job_ref = Mock()
|
|
318
|
+
mock_db.collection.return_value.document.return_value = mock_job_ref
|
|
319
|
+
mock_user_service.return_value.db = mock_db
|
|
320
|
+
|
|
321
|
+
response = client.post("/api/admin/jobs/test-job-123/delete-outputs")
|
|
322
|
+
|
|
323
|
+
assert response.status_code == 200
|
|
324
|
+
mock_logger.info.assert_called()
|
|
325
|
+
log_message = mock_logger.info.call_args[0][0]
|
|
326
|
+
assert "admin" in log_message.lower() or "deleted" in log_message.lower()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestDeleteOutputsAuthorization:
|
|
330
|
+
"""Tests for authorization on the delete outputs endpoint."""
|
|
331
|
+
|
|
332
|
+
def test_requires_admin_access(self, client, mock_complete_job):
|
|
333
|
+
"""Test that non-admin users cannot access the endpoint."""
|
|
334
|
+
original_override = app.dependency_overrides.get(require_admin)
|
|
335
|
+
|
|
336
|
+
def get_non_admin():
|
|
337
|
+
from fastapi import HTTPException
|
|
338
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
339
|
+
|
|
340
|
+
app.dependency_overrides[require_admin] = get_non_admin
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
response = client.post(
|
|
344
|
+
"/api/admin/jobs/test-job-123/delete-outputs",
|
|
345
|
+
headers={"Authorization": "Bearer user-token"}
|
|
346
|
+
)
|
|
347
|
+
assert response.status_code == 403
|
|
348
|
+
finally:
|
|
349
|
+
if original_override:
|
|
350
|
+
app.dependency_overrides[require_admin] = original_override
|
|
351
|
+
else:
|
|
352
|
+
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
|
|