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.
Files changed (55) hide show
  1. backend/api/routes/admin.py +512 -1
  2. backend/api/routes/audio_search.py +17 -34
  3. backend/api/routes/file_upload.py +60 -84
  4. backend/api/routes/internal.py +6 -0
  5. backend/api/routes/jobs.py +11 -3
  6. backend/api/routes/rate_limits.py +428 -0
  7. backend/api/routes/review.py +13 -6
  8. backend/api/routes/tenant.py +120 -0
  9. backend/api/routes/users.py +229 -247
  10. backend/config.py +16 -0
  11. backend/exceptions.py +66 -0
  12. backend/main.py +30 -1
  13. backend/middleware/__init__.py +7 -1
  14. backend/middleware/tenant.py +192 -0
  15. backend/models/job.py +19 -3
  16. backend/models/tenant.py +208 -0
  17. backend/models/user.py +18 -0
  18. backend/services/email_service.py +253 -6
  19. backend/services/email_validation_service.py +646 -0
  20. backend/services/firestore_service.py +27 -0
  21. backend/services/job_defaults_service.py +113 -0
  22. backend/services/job_manager.py +73 -3
  23. backend/services/rate_limit_service.py +641 -0
  24. backend/services/stripe_service.py +61 -35
  25. backend/services/tenant_service.py +285 -0
  26. backend/services/user_service.py +85 -7
  27. backend/tests/conftest.py +7 -1
  28. backend/tests/emulator/test_made_for_you_integration.py +167 -0
  29. backend/tests/test_admin_job_files.py +337 -0
  30. backend/tests/test_admin_job_reset.py +384 -0
  31. backend/tests/test_admin_job_update.py +326 -0
  32. backend/tests/test_audio_search.py +12 -8
  33. backend/tests/test_email_service.py +233 -0
  34. backend/tests/test_email_validation_service.py +298 -0
  35. backend/tests/test_file_upload.py +8 -6
  36. backend/tests/test_impersonation.py +223 -0
  37. backend/tests/test_job_creation_regression.py +4 -0
  38. backend/tests/test_job_manager.py +146 -1
  39. backend/tests/test_made_for_you.py +2088 -0
  40. backend/tests/test_models.py +139 -0
  41. backend/tests/test_rate_limit_service.py +396 -0
  42. backend/tests/test_rate_limits_api.py +392 -0
  43. backend/tests/test_tenant_api.py +350 -0
  44. backend/tests/test_tenant_middleware.py +345 -0
  45. backend/tests/test_tenant_models.py +406 -0
  46. backend/tests/test_tenant_service.py +418 -0
  47. backend/workers/video_worker.py +8 -3
  48. backend/workers/video_worker_orchestrator.py +26 -0
  49. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/METADATA +1 -1
  50. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/RECORD +55 -33
  51. lyrics_transcriber/frontend/src/api.ts +13 -5
  52. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
  53. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/WHEEL +0 -0
  54. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/entry_points.txt +0 -0
  55. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,406 @@
1
+ """
2
+ Unit tests for tenant data models.
3
+
4
+ Tests the TenantConfig, TenantBranding, TenantFeatures, TenantDefaults,
5
+ TenantAuth, and TenantPublicConfig models.
6
+ """
7
+
8
+ import pytest
9
+ from datetime import datetime
10
+
11
+ from backend.models.tenant import (
12
+ TenantConfig,
13
+ TenantBranding,
14
+ TenantFeatures,
15
+ TenantDefaults,
16
+ TenantAuth,
17
+ TenantPublicConfig,
18
+ TenantConfigResponse,
19
+ )
20
+
21
+
22
+ class TestTenantBranding:
23
+ """Tests for TenantBranding model."""
24
+
25
+ def test_default_values(self):
26
+ """Test that TenantBranding has sensible defaults."""
27
+ branding = TenantBranding()
28
+
29
+ assert branding.logo_url is None
30
+ assert branding.logo_height == 40
31
+ assert branding.primary_color == "#ff5bb8"
32
+ assert branding.secondary_color == "#8b5cf6"
33
+ assert branding.accent_color is None
34
+ assert branding.background_color is None
35
+ assert branding.favicon_url is None
36
+ assert branding.site_title == "Karaoke Generator"
37
+ assert branding.tagline is None
38
+
39
+ def test_custom_values(self):
40
+ """Test TenantBranding with custom values."""
41
+ branding = TenantBranding(
42
+ logo_url="https://example.com/logo.png",
43
+ logo_height=60,
44
+ primary_color="#ffff00",
45
+ secondary_color="#0000ff",
46
+ accent_color="#ff0000",
47
+ background_color="#000000",
48
+ favicon_url="https://example.com/favicon.ico",
49
+ site_title="Custom Karaoke",
50
+ tagline="Make music magic",
51
+ )
52
+
53
+ assert branding.logo_url == "https://example.com/logo.png"
54
+ assert branding.logo_height == 60
55
+ assert branding.primary_color == "#ffff00"
56
+ assert branding.site_title == "Custom Karaoke"
57
+
58
+
59
+ class TestTenantFeatures:
60
+ """Tests for TenantFeatures model."""
61
+
62
+ def test_default_features_enabled(self):
63
+ """Test that most features are enabled by default."""
64
+ features = TenantFeatures()
65
+
66
+ # Input methods
67
+ assert features.audio_search is True
68
+ assert features.file_upload is True
69
+ assert features.youtube_url is True
70
+
71
+ # Distribution
72
+ assert features.youtube_upload is True
73
+ assert features.dropbox_upload is True
74
+ assert features.gdrive_upload is True
75
+
76
+ # Customization
77
+ assert features.theme_selection is True
78
+ assert features.color_overrides is True
79
+
80
+ # Output formats
81
+ assert features.enable_cdg is True
82
+ assert features.enable_4k is True
83
+
84
+ # Admin is disabled by default
85
+ assert features.admin_access is False
86
+
87
+ def test_restricted_features(self):
88
+ """Test a restricted feature set like Vocal Star would have."""
89
+ features = TenantFeatures(
90
+ audio_search=False,
91
+ youtube_url=False,
92
+ youtube_upload=False,
93
+ dropbox_upload=False,
94
+ gdrive_upload=False,
95
+ theme_selection=False,
96
+ color_overrides=False,
97
+ )
98
+
99
+ assert features.audio_search is False
100
+ assert features.file_upload is True # Still allowed
101
+ assert features.youtube_url is False
102
+ assert features.youtube_upload is False
103
+
104
+
105
+ class TestTenantDefaults:
106
+ """Tests for TenantDefaults model."""
107
+
108
+ def test_default_values(self):
109
+ """Test TenantDefaults has sensible defaults."""
110
+ defaults = TenantDefaults()
111
+
112
+ assert defaults.theme_id is None
113
+ assert defaults.locked_theme is None
114
+ assert defaults.distribution_mode == "all"
115
+ assert defaults.brand_prefix is None
116
+ assert defaults.youtube_description_template is None
117
+
118
+ def test_locked_theme(self):
119
+ """Test locked_theme setting."""
120
+ defaults = TenantDefaults(
121
+ theme_id="vocalstar",
122
+ locked_theme="vocalstar",
123
+ distribution_mode="download_only",
124
+ brand_prefix="VSTAR",
125
+ )
126
+
127
+ assert defaults.locked_theme == "vocalstar"
128
+ assert defaults.distribution_mode == "download_only"
129
+ assert defaults.brand_prefix == "VSTAR"
130
+
131
+
132
+ class TestTenantAuth:
133
+ """Tests for TenantAuth model."""
134
+
135
+ def test_default_values(self):
136
+ """Test TenantAuth defaults."""
137
+ auth = TenantAuth()
138
+
139
+ assert auth.allowed_email_domains == []
140
+ assert auth.require_email_domain is True
141
+ assert auth.fixed_token_ids == []
142
+ assert auth.sender_email is None
143
+
144
+ def test_restricted_domains(self):
145
+ """Test restricted email domains configuration."""
146
+ auth = TenantAuth(
147
+ allowed_email_domains=["vocal-star.com", "vocalstarmusic.com"],
148
+ require_email_domain=True,
149
+ sender_email="vocalstar@nomadkaraoke.com",
150
+ )
151
+
152
+ assert "vocal-star.com" in auth.allowed_email_domains
153
+ assert auth.require_email_domain is True
154
+ assert auth.sender_email == "vocalstar@nomadkaraoke.com"
155
+
156
+
157
+ class TestTenantConfig:
158
+ """Tests for TenantConfig model."""
159
+
160
+ @pytest.fixture
161
+ def basic_config(self):
162
+ """Create a basic tenant config for testing."""
163
+ return TenantConfig(
164
+ id="vocalstar",
165
+ name="Vocal Star",
166
+ subdomain="vocalstar.nomadkaraoke.com",
167
+ )
168
+
169
+ @pytest.fixture
170
+ def full_config(self):
171
+ """Create a fully configured tenant."""
172
+ return TenantConfig(
173
+ id="vocalstar",
174
+ name="Vocal Star",
175
+ subdomain="vocalstar.nomadkaraoke.com",
176
+ is_active=True,
177
+ branding=TenantBranding(
178
+ logo_url="https://example.com/logo.png",
179
+ primary_color="#ffff00",
180
+ secondary_color="#006CF9",
181
+ site_title="Vocal Star Karaoke Generator",
182
+ ),
183
+ features=TenantFeatures(
184
+ audio_search=False,
185
+ youtube_url=False,
186
+ youtube_upload=False,
187
+ dropbox_upload=False,
188
+ gdrive_upload=False,
189
+ theme_selection=False,
190
+ ),
191
+ defaults=TenantDefaults(
192
+ theme_id="vocalstar",
193
+ locked_theme="vocalstar",
194
+ distribution_mode="download_only",
195
+ ),
196
+ auth=TenantAuth(
197
+ allowed_email_domains=["vocal-star.com", "vocalstarmusic.com"],
198
+ require_email_domain=True,
199
+ sender_email="vocalstar@nomadkaraoke.com",
200
+ ),
201
+ )
202
+
203
+ def test_required_fields(self):
204
+ """Test that id, name, and subdomain are required."""
205
+ config = TenantConfig(
206
+ id="test",
207
+ name="Test Tenant",
208
+ subdomain="test.nomadkaraoke.com",
209
+ )
210
+
211
+ assert config.id == "test"
212
+ assert config.name == "Test Tenant"
213
+ assert config.subdomain == "test.nomadkaraoke.com"
214
+
215
+ def test_default_is_active(self, basic_config):
216
+ """Test that tenants are active by default."""
217
+ assert basic_config.is_active is True
218
+
219
+ def test_nested_models_have_defaults(self, basic_config):
220
+ """Test that nested models are created with defaults."""
221
+ assert basic_config.branding is not None
222
+ assert basic_config.features is not None
223
+ assert basic_config.defaults is not None
224
+ assert basic_config.auth is not None
225
+
226
+ # Tests for get_sender_email()
227
+ def test_get_sender_email_custom(self, full_config):
228
+ """Test get_sender_email returns custom sender when configured."""
229
+ assert full_config.get_sender_email() == "vocalstar@nomadkaraoke.com"
230
+
231
+ def test_get_sender_email_default_pattern(self, basic_config):
232
+ """Test get_sender_email returns default pattern when not configured."""
233
+ # basic_config has no custom sender_email
234
+ assert basic_config.get_sender_email() == "vocalstar@nomadkaraoke.com"
235
+
236
+ def test_get_sender_email_different_tenant(self):
237
+ """Test sender email follows tenant ID pattern."""
238
+ config = TenantConfig(
239
+ id="customtenant",
240
+ name="Custom",
241
+ subdomain="custom.nomadkaraoke.com",
242
+ )
243
+ assert config.get_sender_email() == "customtenant@nomadkaraoke.com"
244
+
245
+ # Tests for is_email_allowed()
246
+ def test_is_email_allowed_matching_domain(self, full_config):
247
+ """Test email is allowed when domain matches."""
248
+ assert full_config.is_email_allowed("user@vocal-star.com") is True
249
+ assert full_config.is_email_allowed("user@vocalstarmusic.com") is True
250
+
251
+ def test_is_email_allowed_non_matching_domain(self, full_config):
252
+ """Test email is rejected when domain doesn't match and require_email_domain=True."""
253
+ assert full_config.is_email_allowed("user@gmail.com") is False
254
+ assert full_config.is_email_allowed("user@other.com") is False
255
+
256
+ def test_is_email_allowed_case_insensitive(self, full_config):
257
+ """Test email domain matching is case insensitive."""
258
+ assert full_config.is_email_allowed("User@VOCAL-STAR.COM") is True
259
+ assert full_config.is_email_allowed("User@VocalStarMusic.com") is True
260
+
261
+ def test_is_email_allowed_no_domain_restrictions(self, basic_config):
262
+ """Test any email allowed when no domain restrictions."""
263
+ # basic_config has empty allowed_email_domains
264
+ assert basic_config.is_email_allowed("anyone@gmail.com") is True
265
+ assert basic_config.is_email_allowed("user@anything.com") is True
266
+
267
+ def test_is_email_allowed_require_domain_false(self):
268
+ """Test non-matching emails allowed when require_email_domain=False."""
269
+ config = TenantConfig(
270
+ id="flexible",
271
+ name="Flexible Tenant",
272
+ subdomain="flexible.nomadkaraoke.com",
273
+ auth=TenantAuth(
274
+ allowed_email_domains=["preferred.com"],
275
+ require_email_domain=False, # Don't require matching
276
+ ),
277
+ )
278
+
279
+ # Matching domain still works
280
+ assert config.is_email_allowed("user@preferred.com") is True
281
+ # Non-matching also allowed since require_email_domain=False
282
+ assert config.is_email_allowed("user@other.com") is True
283
+
284
+ def test_is_email_allowed_partial_domain_no_match(self, full_config):
285
+ """Test partial domain matches don't work (must be exact suffix)."""
286
+ # "star.com" should not match "vocal-star.com"
287
+ assert full_config.is_email_allowed("user@star.com") is False
288
+ # Subdomain of allowed domain should work
289
+ assert full_config.is_email_allowed("user@sub.vocal-star.com") is False
290
+
291
+
292
+ class TestTenantPublicConfig:
293
+ """Tests for TenantPublicConfig model."""
294
+
295
+ @pytest.fixture
296
+ def full_config(self):
297
+ """Create a fully configured tenant for conversion testing."""
298
+ return TenantConfig(
299
+ id="vocalstar",
300
+ name="Vocal Star",
301
+ subdomain="vocalstar.nomadkaraoke.com",
302
+ is_active=True,
303
+ branding=TenantBranding(
304
+ logo_url="https://example.com/logo.png",
305
+ primary_color="#ffff00",
306
+ ),
307
+ features=TenantFeatures(
308
+ audio_search=False,
309
+ admin_access=True, # This should be included in public config
310
+ ),
311
+ defaults=TenantDefaults(
312
+ theme_id="vocalstar",
313
+ locked_theme="vocalstar",
314
+ distribution_mode="download_only",
315
+ brand_prefix="VSTAR", # This should NOT be in public config
316
+ youtube_description_template="Custom template", # This should NOT be in public config
317
+ ),
318
+ auth=TenantAuth(
319
+ allowed_email_domains=["vocal-star.com"],
320
+ require_email_domain=True,
321
+ fixed_token_ids=["secret-token-123"], # This should NOT be in public config
322
+ sender_email="vocalstar@nomadkaraoke.com", # This should NOT be in public config
323
+ ),
324
+ )
325
+
326
+ def test_from_config_basic_fields(self, full_config):
327
+ """Test that basic fields are copied correctly."""
328
+ public = TenantPublicConfig.from_config(full_config)
329
+
330
+ assert public.id == "vocalstar"
331
+ assert public.name == "Vocal Star"
332
+ assert public.subdomain == "vocalstar.nomadkaraoke.com"
333
+ assert public.is_active is True
334
+
335
+ def test_from_config_branding_included(self, full_config):
336
+ """Test that branding is fully included."""
337
+ public = TenantPublicConfig.from_config(full_config)
338
+
339
+ assert public.branding.logo_url == "https://example.com/logo.png"
340
+ assert public.branding.primary_color == "#ffff00"
341
+
342
+ def test_from_config_features_included(self, full_config):
343
+ """Test that features are fully included."""
344
+ public = TenantPublicConfig.from_config(full_config)
345
+
346
+ assert public.features.audio_search is False
347
+ assert public.features.admin_access is True
348
+
349
+ def test_from_config_allowed_email_domains_included(self, full_config):
350
+ """Test that allowed_email_domains is included for frontend validation."""
351
+ public = TenantPublicConfig.from_config(full_config)
352
+
353
+ assert "vocal-star.com" in public.allowed_email_domains
354
+
355
+ def test_from_config_sensitive_auth_excluded(self, full_config):
356
+ """Test that sensitive auth fields are not in public config."""
357
+ public = TenantPublicConfig.from_config(full_config)
358
+
359
+ # TenantPublicConfig only has allowed_email_domains from auth
360
+ # It should NOT have fixed_token_ids or sender_email
361
+ assert not hasattr(public, 'auth')
362
+ # allowed_email_domains is a top-level field
363
+ assert hasattr(public, 'allowed_email_domains')
364
+
365
+ def test_from_config_defaults_partially_included(self, full_config):
366
+ """Test that only safe defaults are included."""
367
+ public = TenantPublicConfig.from_config(full_config)
368
+
369
+ # These should be included
370
+ assert public.defaults.theme_id == "vocalstar"
371
+ assert public.defaults.locked_theme == "vocalstar"
372
+ assert public.defaults.distribution_mode == "download_only"
373
+
374
+ # brand_prefix and youtube_description_template should NOT be included
375
+ # The from_config method creates a new TenantDefaults without these
376
+ assert public.defaults.brand_prefix is None
377
+ assert public.defaults.youtube_description_template is None
378
+
379
+
380
+ class TestTenantConfigResponse:
381
+ """Tests for TenantConfigResponse model."""
382
+
383
+ def test_default_response(self):
384
+ """Test default response indicates no tenant."""
385
+ response = TenantConfigResponse()
386
+
387
+ assert response.tenant is None
388
+ assert response.is_default is True
389
+
390
+ def test_tenant_response(self):
391
+ """Test response with tenant config."""
392
+ public_config = TenantPublicConfig(
393
+ id="vocalstar",
394
+ name="Vocal Star",
395
+ subdomain="vocalstar.nomadkaraoke.com",
396
+ is_active=True,
397
+ branding=TenantBranding(),
398
+ features=TenantFeatures(),
399
+ defaults=TenantDefaults(),
400
+ )
401
+
402
+ response = TenantConfigResponse(tenant=public_config, is_default=False)
403
+
404
+ assert response.tenant is not None
405
+ assert response.tenant.id == "vocalstar"
406
+ assert response.is_default is False