karaoke-gen 0.99.3__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 +512 -1
- backend/api/routes/audio_search.py +13 -2
- backend/api/routes/file_upload.py +42 -1
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +9 -1
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +167 -245
- backend/main.py +6 -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/firestore_service.py +6 -0
- backend/services/job_manager.py +32 -1
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- 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 +146 -1
- backend/tests/test_made_for_you.py +2086 -0
- backend/tests/test_models.py +139 -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
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/RECORD +42 -28
- 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.101.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.101.0.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
|