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
backend/tests/test_models.py
CHANGED
|
@@ -913,6 +913,145 @@ class TestJobWithFullTwoPhaseConfig:
|
|
|
913
913
|
assert "title" in job.file_urls["screens"]
|
|
914
914
|
|
|
915
915
|
|
|
916
|
+
class TestMadeForYouFields:
|
|
917
|
+
"""Tests for made-for-you order tracking fields."""
|
|
918
|
+
|
|
919
|
+
def test_job_has_made_for_you_field(self):
|
|
920
|
+
"""Test Job model has made_for_you field with default False."""
|
|
921
|
+
job = Job(
|
|
922
|
+
job_id="test123",
|
|
923
|
+
status=JobStatus.PENDING,
|
|
924
|
+
created_at=datetime.now(UTC),
|
|
925
|
+
updated_at=datetime.now(UTC),
|
|
926
|
+
)
|
|
927
|
+
assert hasattr(job, 'made_for_you')
|
|
928
|
+
assert job.made_for_you is False
|
|
929
|
+
|
|
930
|
+
def test_job_made_for_you_true(self):
|
|
931
|
+
"""Test Job model can set made_for_you to True."""
|
|
932
|
+
job = Job(
|
|
933
|
+
job_id="test123",
|
|
934
|
+
status=JobStatus.PENDING,
|
|
935
|
+
created_at=datetime.now(UTC),
|
|
936
|
+
updated_at=datetime.now(UTC),
|
|
937
|
+
made_for_you=True,
|
|
938
|
+
)
|
|
939
|
+
assert job.made_for_you is True
|
|
940
|
+
|
|
941
|
+
def test_job_has_customer_email_field(self):
|
|
942
|
+
"""Test Job model has customer_email field with default None."""
|
|
943
|
+
job = Job(
|
|
944
|
+
job_id="test123",
|
|
945
|
+
status=JobStatus.PENDING,
|
|
946
|
+
created_at=datetime.now(UTC),
|
|
947
|
+
updated_at=datetime.now(UTC),
|
|
948
|
+
)
|
|
949
|
+
assert hasattr(job, 'customer_email')
|
|
950
|
+
assert job.customer_email is None
|
|
951
|
+
|
|
952
|
+
def test_job_customer_email_set(self):
|
|
953
|
+
"""Test Job model can set customer_email."""
|
|
954
|
+
job = Job(
|
|
955
|
+
job_id="test123",
|
|
956
|
+
status=JobStatus.PENDING,
|
|
957
|
+
created_at=datetime.now(UTC),
|
|
958
|
+
updated_at=datetime.now(UTC),
|
|
959
|
+
customer_email="customer@example.com",
|
|
960
|
+
)
|
|
961
|
+
assert job.customer_email == "customer@example.com"
|
|
962
|
+
|
|
963
|
+
def test_job_has_customer_notes_field(self):
|
|
964
|
+
"""Test Job model has customer_notes field with default None."""
|
|
965
|
+
job = Job(
|
|
966
|
+
job_id="test123",
|
|
967
|
+
status=JobStatus.PENDING,
|
|
968
|
+
created_at=datetime.now(UTC),
|
|
969
|
+
updated_at=datetime.now(UTC),
|
|
970
|
+
)
|
|
971
|
+
assert hasattr(job, 'customer_notes')
|
|
972
|
+
assert job.customer_notes is None
|
|
973
|
+
|
|
974
|
+
def test_job_customer_notes_set(self):
|
|
975
|
+
"""Test Job model can set customer_notes."""
|
|
976
|
+
job = Job(
|
|
977
|
+
job_id="test123",
|
|
978
|
+
status=JobStatus.PENDING,
|
|
979
|
+
created_at=datetime.now(UTC),
|
|
980
|
+
updated_at=datetime.now(UTC),
|
|
981
|
+
customer_notes="Please make it extra special!",
|
|
982
|
+
)
|
|
983
|
+
assert job.customer_notes == "Please make it extra special!"
|
|
984
|
+
|
|
985
|
+
def test_job_create_has_made_for_you_fields(self):
|
|
986
|
+
"""Test JobCreate model has all made-for-you fields."""
|
|
987
|
+
job_create = JobCreate(
|
|
988
|
+
artist="Test Artist",
|
|
989
|
+
title="Test Song",
|
|
990
|
+
)
|
|
991
|
+
assert hasattr(job_create, 'made_for_you')
|
|
992
|
+
assert hasattr(job_create, 'customer_email')
|
|
993
|
+
assert hasattr(job_create, 'customer_notes')
|
|
994
|
+
assert job_create.made_for_you is False
|
|
995
|
+
assert job_create.customer_email is None
|
|
996
|
+
assert job_create.customer_notes is None
|
|
997
|
+
|
|
998
|
+
def test_job_create_with_made_for_you_config(self):
|
|
999
|
+
"""Test JobCreate with full made-for-you configuration."""
|
|
1000
|
+
job_create = JobCreate(
|
|
1001
|
+
artist="Seether",
|
|
1002
|
+
title="Tonight",
|
|
1003
|
+
user_email="admin@nomadkaraoke.com",
|
|
1004
|
+
made_for_you=True,
|
|
1005
|
+
customer_email="customer@example.com",
|
|
1006
|
+
customer_notes="Wedding anniversary!",
|
|
1007
|
+
)
|
|
1008
|
+
assert job_create.made_for_you is True
|
|
1009
|
+
assert job_create.customer_email == "customer@example.com"
|
|
1010
|
+
assert job_create.customer_notes == "Wedding anniversary!"
|
|
1011
|
+
assert job_create.user_email == "admin@nomadkaraoke.com"
|
|
1012
|
+
|
|
1013
|
+
def test_made_for_you_serialization_roundtrip(self):
|
|
1014
|
+
"""Test made-for-you fields survive dict serialization."""
|
|
1015
|
+
job = Job(
|
|
1016
|
+
job_id="test123",
|
|
1017
|
+
status=JobStatus.AWAITING_AUDIO_SELECTION,
|
|
1018
|
+
created_at=datetime.now(UTC),
|
|
1019
|
+
updated_at=datetime.now(UTC),
|
|
1020
|
+
made_for_you=True,
|
|
1021
|
+
customer_email="customer@example.com",
|
|
1022
|
+
customer_notes="Test notes",
|
|
1023
|
+
user_email="admin@nomadkaraoke.com",
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
job_dict = job.model_dump()
|
|
1027
|
+
|
|
1028
|
+
assert job_dict['made_for_you'] is True
|
|
1029
|
+
assert job_dict['customer_email'] == "customer@example.com"
|
|
1030
|
+
assert job_dict['customer_notes'] == "Test notes"
|
|
1031
|
+
|
|
1032
|
+
def test_made_for_you_job_with_distribution_settings(self):
|
|
1033
|
+
"""Test made-for-you job with distribution settings."""
|
|
1034
|
+
job = Job(
|
|
1035
|
+
job_id="test123",
|
|
1036
|
+
status=JobStatus.AWAITING_AUDIO_SELECTION,
|
|
1037
|
+
created_at=datetime.now(UTC),
|
|
1038
|
+
updated_at=datetime.now(UTC),
|
|
1039
|
+
made_for_you=True,
|
|
1040
|
+
customer_email="customer@example.com",
|
|
1041
|
+
user_email="admin@nomadkaraoke.com",
|
|
1042
|
+
enable_youtube_upload=True,
|
|
1043
|
+
dropbox_path="/Production/Ready To Upload",
|
|
1044
|
+
gdrive_folder_id="1ABC123",
|
|
1045
|
+
brand_prefix="NOMAD",
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
assert job.made_for_you is True
|
|
1049
|
+
assert job.enable_youtube_upload is True
|
|
1050
|
+
assert job.dropbox_path == "/Production/Ready To Upload"
|
|
1051
|
+
assert job.gdrive_folder_id == "1ABC123"
|
|
1052
|
+
assert job.brand_prefix == "NOMAD"
|
|
1053
|
+
|
|
1054
|
+
|
|
916
1055
|
if __name__ == "__main__":
|
|
917
1056
|
pytest.main([__file__, "-v"])
|
|
918
1057
|
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for tenant API routes.
|
|
3
|
+
|
|
4
|
+
Tests the /api/tenant/config endpoints.
|
|
5
|
+
|
|
6
|
+
Note: We need to patch get_tenant_service in both the routes module AND the
|
|
7
|
+
middleware module since the middleware runs first and also calls get_tenant_service.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
from fastapi.testclient import TestClient
|
|
13
|
+
|
|
14
|
+
from backend.models.tenant import (
|
|
15
|
+
TenantConfig,
|
|
16
|
+
TenantBranding,
|
|
17
|
+
TenantFeatures,
|
|
18
|
+
TenantDefaults,
|
|
19
|
+
TenantAuth,
|
|
20
|
+
TenantPublicConfig,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Sample tenant config for mocking
|
|
25
|
+
SAMPLE_VOCALSTAR_CONFIG = TenantConfig(
|
|
26
|
+
id="vocalstar",
|
|
27
|
+
name="Vocal Star",
|
|
28
|
+
subdomain="vocalstar.nomadkaraoke.com",
|
|
29
|
+
is_active=True,
|
|
30
|
+
branding=TenantBranding(
|
|
31
|
+
logo_url="https://example.com/logo.png",
|
|
32
|
+
primary_color="#ffff00",
|
|
33
|
+
secondary_color="#006CF9",
|
|
34
|
+
site_title="Vocal Star Karaoke Generator",
|
|
35
|
+
),
|
|
36
|
+
features=TenantFeatures(
|
|
37
|
+
audio_search=False,
|
|
38
|
+
youtube_url=False,
|
|
39
|
+
youtube_upload=False,
|
|
40
|
+
dropbox_upload=False,
|
|
41
|
+
gdrive_upload=False,
|
|
42
|
+
theme_selection=False,
|
|
43
|
+
),
|
|
44
|
+
defaults=TenantDefaults(
|
|
45
|
+
theme_id="vocalstar",
|
|
46
|
+
locked_theme="vocalstar",
|
|
47
|
+
distribution_mode="download_only",
|
|
48
|
+
),
|
|
49
|
+
auth=TenantAuth(
|
|
50
|
+
allowed_email_domains=["vocal-star.com", "vocalstarmusic.com"],
|
|
51
|
+
require_email_domain=True,
|
|
52
|
+
sender_email="vocalstar@nomadkaraoke.com",
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
INACTIVE_CONFIG = TenantConfig(
|
|
57
|
+
id="inactive",
|
|
58
|
+
name="Inactive Tenant",
|
|
59
|
+
subdomain="inactive.nomadkaraoke.com",
|
|
60
|
+
is_active=False,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestTenantConfigEndpoint:
|
|
65
|
+
"""Tests for GET /api/tenant/config endpoint."""
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def mock_tenant_service(self):
|
|
69
|
+
"""Mock the tenant service in both routes and middleware."""
|
|
70
|
+
service = MagicMock()
|
|
71
|
+
# Patch in both places: routes AND middleware
|
|
72
|
+
with patch("backend.api.routes.tenant.get_tenant_service", return_value=service), \
|
|
73
|
+
patch("backend.middleware.tenant.get_tenant_service", return_value=service):
|
|
74
|
+
yield service
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def client(self, mock_tenant_service):
|
|
78
|
+
"""Create test client with mocked tenant service."""
|
|
79
|
+
from backend.main import app
|
|
80
|
+
return TestClient(app)
|
|
81
|
+
|
|
82
|
+
def test_get_config_default(self, client, mock_tenant_service):
|
|
83
|
+
"""Test returns default config when no tenant detected."""
|
|
84
|
+
mock_tenant_service.tenant_exists.return_value = False
|
|
85
|
+
|
|
86
|
+
response = client.get("/api/tenant/config")
|
|
87
|
+
|
|
88
|
+
assert response.status_code == 200
|
|
89
|
+
data = response.json()
|
|
90
|
+
assert data["tenant"] is None
|
|
91
|
+
assert data["is_default"] is True
|
|
92
|
+
|
|
93
|
+
def test_get_config_with_query_param(self, client, mock_tenant_service):
|
|
94
|
+
"""Test tenant detection via query parameter."""
|
|
95
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
96
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
97
|
+
|
|
98
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
99
|
+
|
|
100
|
+
assert response.status_code == 200
|
|
101
|
+
data = response.json()
|
|
102
|
+
assert data["tenant"]["id"] == "vocalstar"
|
|
103
|
+
assert data["tenant"]["name"] == "Vocal Star"
|
|
104
|
+
assert data["is_default"] is False
|
|
105
|
+
|
|
106
|
+
def test_get_config_with_header(self, client, mock_tenant_service):
|
|
107
|
+
"""Test tenant detection via X-Tenant-ID header."""
|
|
108
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
109
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
110
|
+
|
|
111
|
+
response = client.get(
|
|
112
|
+
"/api/tenant/config",
|
|
113
|
+
headers={"X-Tenant-ID": "vocalstar"},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert response.status_code == 200
|
|
117
|
+
data = response.json()
|
|
118
|
+
assert data["tenant"]["id"] == "vocalstar"
|
|
119
|
+
assert data["is_default"] is False
|
|
120
|
+
|
|
121
|
+
def test_get_config_query_param_takes_priority(self, client, mock_tenant_service):
|
|
122
|
+
"""Test query param takes priority over header."""
|
|
123
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
124
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
125
|
+
|
|
126
|
+
response = client.get(
|
|
127
|
+
"/api/tenant/config?tenant=vocalstar",
|
|
128
|
+
headers={"X-Tenant-ID": "other"},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
assert response.status_code == 200
|
|
132
|
+
# Should use vocalstar from query param
|
|
133
|
+
mock_tenant_service.get_public_config.assert_called_with("vocalstar")
|
|
134
|
+
|
|
135
|
+
def test_get_config_tenant_not_found(self, client, mock_tenant_service):
|
|
136
|
+
"""Test returns default when tenant not found."""
|
|
137
|
+
mock_tenant_service.get_public_config.return_value = None
|
|
138
|
+
|
|
139
|
+
response = client.get("/api/tenant/config?tenant=nonexistent")
|
|
140
|
+
|
|
141
|
+
assert response.status_code == 200
|
|
142
|
+
data = response.json()
|
|
143
|
+
assert data["tenant"] is None
|
|
144
|
+
assert data["is_default"] is True
|
|
145
|
+
|
|
146
|
+
def test_get_config_inactive_tenant(self, client, mock_tenant_service):
|
|
147
|
+
"""Test returns default when tenant is inactive."""
|
|
148
|
+
inactive_public = TenantPublicConfig.from_config(INACTIVE_CONFIG)
|
|
149
|
+
mock_tenant_service.get_public_config.return_value = inactive_public
|
|
150
|
+
|
|
151
|
+
response = client.get("/api/tenant/config?tenant=inactive")
|
|
152
|
+
|
|
153
|
+
assert response.status_code == 200
|
|
154
|
+
data = response.json()
|
|
155
|
+
assert data["tenant"] is None
|
|
156
|
+
assert data["is_default"] is True
|
|
157
|
+
|
|
158
|
+
def test_get_config_includes_branding(self, client, mock_tenant_service):
|
|
159
|
+
"""Test response includes full branding config."""
|
|
160
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
161
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
162
|
+
|
|
163
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
164
|
+
|
|
165
|
+
assert response.status_code == 200
|
|
166
|
+
data = response.json()
|
|
167
|
+
branding = data["tenant"]["branding"]
|
|
168
|
+
assert branding["primary_color"] == "#ffff00"
|
|
169
|
+
assert branding["secondary_color"] == "#006CF9"
|
|
170
|
+
assert branding["site_title"] == "Vocal Star Karaoke Generator"
|
|
171
|
+
|
|
172
|
+
def test_get_config_includes_features(self, client, mock_tenant_service):
|
|
173
|
+
"""Test response includes feature flags."""
|
|
174
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
175
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
176
|
+
|
|
177
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
178
|
+
|
|
179
|
+
assert response.status_code == 200
|
|
180
|
+
data = response.json()
|
|
181
|
+
features = data["tenant"]["features"]
|
|
182
|
+
assert features["audio_search"] is False
|
|
183
|
+
assert features["file_upload"] is True
|
|
184
|
+
assert features["youtube_upload"] is False
|
|
185
|
+
|
|
186
|
+
def test_get_config_includes_defaults(self, client, mock_tenant_service):
|
|
187
|
+
"""Test response includes default settings."""
|
|
188
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
189
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
190
|
+
|
|
191
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
192
|
+
|
|
193
|
+
assert response.status_code == 200
|
|
194
|
+
data = response.json()
|
|
195
|
+
defaults = data["tenant"]["defaults"]
|
|
196
|
+
assert defaults["theme_id"] == "vocalstar"
|
|
197
|
+
assert defaults["locked_theme"] == "vocalstar"
|
|
198
|
+
assert defaults["distribution_mode"] == "download_only"
|
|
199
|
+
|
|
200
|
+
def test_get_config_includes_allowed_domains(self, client, mock_tenant_service):
|
|
201
|
+
"""Test response includes allowed email domains."""
|
|
202
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
203
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
204
|
+
|
|
205
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
206
|
+
|
|
207
|
+
assert response.status_code == 200
|
|
208
|
+
data = response.json()
|
|
209
|
+
assert "vocal-star.com" in data["tenant"]["allowed_email_domains"]
|
|
210
|
+
assert "vocalstarmusic.com" in data["tenant"]["allowed_email_domains"]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestTenantConfigByIdEndpoint:
|
|
214
|
+
"""Tests for GET /api/tenant/config/{tenant_id} endpoint."""
|
|
215
|
+
|
|
216
|
+
@pytest.fixture
|
|
217
|
+
def mock_tenant_service(self):
|
|
218
|
+
"""Mock the tenant service in both routes and middleware."""
|
|
219
|
+
service = MagicMock()
|
|
220
|
+
with patch("backend.api.routes.tenant.get_tenant_service", return_value=service), \
|
|
221
|
+
patch("backend.middleware.tenant.get_tenant_service", return_value=service):
|
|
222
|
+
yield service
|
|
223
|
+
|
|
224
|
+
@pytest.fixture
|
|
225
|
+
def client(self, mock_tenant_service):
|
|
226
|
+
"""Create test client with mocked tenant service."""
|
|
227
|
+
from backend.main import app
|
|
228
|
+
return TestClient(app)
|
|
229
|
+
|
|
230
|
+
def test_get_config_by_id_success(self, client, mock_tenant_service):
|
|
231
|
+
"""Test getting config by explicit ID."""
|
|
232
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
233
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
234
|
+
|
|
235
|
+
response = client.get("/api/tenant/config/vocalstar")
|
|
236
|
+
|
|
237
|
+
assert response.status_code == 200
|
|
238
|
+
data = response.json()
|
|
239
|
+
assert data["tenant"]["id"] == "vocalstar"
|
|
240
|
+
assert data["is_default"] is False
|
|
241
|
+
|
|
242
|
+
def test_get_config_by_id_not_found(self, client, mock_tenant_service):
|
|
243
|
+
"""Test returns default when tenant ID not found."""
|
|
244
|
+
mock_tenant_service.get_public_config.return_value = None
|
|
245
|
+
|
|
246
|
+
response = client.get("/api/tenant/config/nonexistent")
|
|
247
|
+
|
|
248
|
+
assert response.status_code == 200
|
|
249
|
+
data = response.json()
|
|
250
|
+
assert data["tenant"] is None
|
|
251
|
+
assert data["is_default"] is True
|
|
252
|
+
|
|
253
|
+
def test_get_config_by_id_inactive(self, client, mock_tenant_service):
|
|
254
|
+
"""Test returns default when tenant is inactive."""
|
|
255
|
+
inactive_public = TenantPublicConfig.from_config(INACTIVE_CONFIG)
|
|
256
|
+
mock_tenant_service.get_public_config.return_value = inactive_public
|
|
257
|
+
|
|
258
|
+
response = client.get("/api/tenant/config/inactive")
|
|
259
|
+
|
|
260
|
+
assert response.status_code == 200
|
|
261
|
+
data = response.json()
|
|
262
|
+
assert data["tenant"] is None
|
|
263
|
+
assert data["is_default"] is True
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class TestTenantAssetEndpoint:
|
|
267
|
+
"""Tests for GET /api/tenant/asset/{tenant_id}/{asset_name} endpoint."""
|
|
268
|
+
|
|
269
|
+
@pytest.fixture
|
|
270
|
+
def mock_tenant_service(self):
|
|
271
|
+
"""Mock the tenant service in both routes and middleware."""
|
|
272
|
+
service = MagicMock()
|
|
273
|
+
with patch("backend.api.routes.tenant.get_tenant_service", return_value=service), \
|
|
274
|
+
patch("backend.middleware.tenant.get_tenant_service", return_value=service):
|
|
275
|
+
yield service
|
|
276
|
+
|
|
277
|
+
@pytest.fixture
|
|
278
|
+
def client(self, mock_tenant_service):
|
|
279
|
+
"""Create test client with mocked tenant service."""
|
|
280
|
+
from backend.main import app
|
|
281
|
+
return TestClient(app)
|
|
282
|
+
|
|
283
|
+
def test_get_asset_redirects(self, client, mock_tenant_service):
|
|
284
|
+
"""Test asset endpoint redirects to signed URL."""
|
|
285
|
+
mock_tenant_service.get_asset_url.return_value = "https://storage.googleapis.com/signed-url"
|
|
286
|
+
|
|
287
|
+
response = client.get(
|
|
288
|
+
"/api/tenant/asset/vocalstar/logo.png",
|
|
289
|
+
follow_redirects=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Should redirect (307 Temporary Redirect is FastAPI default)
|
|
293
|
+
assert response.status_code == 307
|
|
294
|
+
assert "storage.googleapis.com" in response.headers["location"]
|
|
295
|
+
|
|
296
|
+
def test_get_asset_not_found(self, client, mock_tenant_service):
|
|
297
|
+
"""Test returns 404 when asset not found."""
|
|
298
|
+
mock_tenant_service.get_asset_url.return_value = None
|
|
299
|
+
|
|
300
|
+
response = client.get("/api/tenant/asset/vocalstar/missing.png")
|
|
301
|
+
|
|
302
|
+
assert response.status_code == 404
|
|
303
|
+
assert "not found" in response.json()["detail"].lower()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestTenantConfigResponseSchema:
|
|
307
|
+
"""Tests for response schema validation."""
|
|
308
|
+
|
|
309
|
+
@pytest.fixture
|
|
310
|
+
def mock_tenant_service(self):
|
|
311
|
+
"""Mock the tenant service in both routes and middleware."""
|
|
312
|
+
service = MagicMock()
|
|
313
|
+
with patch("backend.api.routes.tenant.get_tenant_service", return_value=service), \
|
|
314
|
+
patch("backend.middleware.tenant.get_tenant_service", return_value=service):
|
|
315
|
+
yield service
|
|
316
|
+
|
|
317
|
+
@pytest.fixture
|
|
318
|
+
def client(self, mock_tenant_service):
|
|
319
|
+
"""Create test client with mocked tenant service."""
|
|
320
|
+
from backend.main import app
|
|
321
|
+
return TestClient(app)
|
|
322
|
+
|
|
323
|
+
def test_response_has_required_fields(self, client, mock_tenant_service):
|
|
324
|
+
"""Test response always has required fields."""
|
|
325
|
+
mock_tenant_service.tenant_exists.return_value = False
|
|
326
|
+
|
|
327
|
+
response = client.get("/api/tenant/config")
|
|
328
|
+
|
|
329
|
+
assert response.status_code == 200
|
|
330
|
+
data = response.json()
|
|
331
|
+
assert "tenant" in data
|
|
332
|
+
assert "is_default" in data
|
|
333
|
+
|
|
334
|
+
def test_tenant_config_excludes_sensitive_fields(self, client, mock_tenant_service):
|
|
335
|
+
"""Test public config doesn't include sensitive auth data."""
|
|
336
|
+
public_config = TenantPublicConfig.from_config(SAMPLE_VOCALSTAR_CONFIG)
|
|
337
|
+
mock_tenant_service.get_public_config.return_value = public_config
|
|
338
|
+
|
|
339
|
+
response = client.get("/api/tenant/config?tenant=vocalstar")
|
|
340
|
+
|
|
341
|
+
assert response.status_code == 200
|
|
342
|
+
data = response.json()
|
|
343
|
+
tenant = data["tenant"]
|
|
344
|
+
|
|
345
|
+
# Should NOT have full auth object
|
|
346
|
+
assert "auth" not in tenant
|
|
347
|
+
# Should NOT have sensitive defaults
|
|
348
|
+
assert tenant["defaults"].get("brand_prefix") is None
|
|
349
|
+
# Should have allowed_email_domains at top level
|
|
350
|
+
assert "allowed_email_domains" in tenant
|