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,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Push Notification API routes.
|
|
3
|
+
|
|
4
|
+
Tests the /api/push/* endpoints for subscription management.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch, AsyncMock
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from backend.main import app
|
|
13
|
+
from backend.api.dependencies import require_auth, require_admin
|
|
14
|
+
from backend.services.auth_service import UserType, AuthResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client():
|
|
19
|
+
"""Create test client."""
|
|
20
|
+
return TestClient(app)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_auth_result():
|
|
25
|
+
"""Create a mock AuthResult for regular user."""
|
|
26
|
+
return AuthResult(
|
|
27
|
+
is_valid=True,
|
|
28
|
+
user_type=UserType.UNLIMITED,
|
|
29
|
+
remaining_uses=-1,
|
|
30
|
+
message="Valid",
|
|
31
|
+
user_email="test@example.com",
|
|
32
|
+
is_admin=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def mock_admin_auth_result():
|
|
38
|
+
"""Create a mock AuthResult for admin user."""
|
|
39
|
+
return AuthResult(
|
|
40
|
+
is_valid=True,
|
|
41
|
+
user_type=UserType.ADMIN,
|
|
42
|
+
remaining_uses=-1,
|
|
43
|
+
message="Valid",
|
|
44
|
+
user_email="admin@nomadkaraoke.com",
|
|
45
|
+
is_admin=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestGetVapidPublicKey:
|
|
50
|
+
"""Tests for GET /api/push/vapid-public-key."""
|
|
51
|
+
|
|
52
|
+
def test_returns_disabled_when_feature_off(self, client):
|
|
53
|
+
"""Returns enabled=false when push notifications disabled."""
|
|
54
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
55
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
56
|
+
|
|
57
|
+
response = client.get("/api/push/vapid-public-key")
|
|
58
|
+
|
|
59
|
+
assert response.status_code == 200
|
|
60
|
+
data = response.json()
|
|
61
|
+
assert data["enabled"] is False
|
|
62
|
+
assert data["vapid_public_key"] is None
|
|
63
|
+
|
|
64
|
+
def test_returns_key_when_enabled(self, client):
|
|
65
|
+
"""Returns public key when push notifications enabled."""
|
|
66
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
67
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
68
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
69
|
+
mock_service.return_value.get_public_key.return_value = "test-public-key-123"
|
|
70
|
+
|
|
71
|
+
response = client.get("/api/push/vapid-public-key")
|
|
72
|
+
|
|
73
|
+
assert response.status_code == 200
|
|
74
|
+
data = response.json()
|
|
75
|
+
assert data["enabled"] is True
|
|
76
|
+
assert data["vapid_public_key"] == "test-public-key-123"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestSubscribe:
|
|
80
|
+
"""Tests for POST /api/push/subscribe."""
|
|
81
|
+
|
|
82
|
+
def test_requires_authentication(self, client):
|
|
83
|
+
"""Returns 401 when not authenticated."""
|
|
84
|
+
# Clear any auth overrides to test real auth
|
|
85
|
+
app.dependency_overrides.clear()
|
|
86
|
+
|
|
87
|
+
response = client.post(
|
|
88
|
+
"/api/push/subscribe",
|
|
89
|
+
json={
|
|
90
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
91
|
+
"keys": {"p256dh": "key1", "auth": "key2"}
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert response.status_code == 401
|
|
96
|
+
|
|
97
|
+
def test_returns_503_when_disabled(self, client, mock_auth_result):
|
|
98
|
+
"""Returns 503 when push notifications disabled."""
|
|
99
|
+
async def override_auth():
|
|
100
|
+
return mock_auth_result
|
|
101
|
+
|
|
102
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
106
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
107
|
+
|
|
108
|
+
response = client.post(
|
|
109
|
+
"/api/push/subscribe",
|
|
110
|
+
json={
|
|
111
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
112
|
+
"keys": {"p256dh": "key1", "auth": "key2"}
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert response.status_code == 503
|
|
117
|
+
assert "not enabled" in response.json()["detail"].lower()
|
|
118
|
+
finally:
|
|
119
|
+
app.dependency_overrides.clear()
|
|
120
|
+
|
|
121
|
+
def test_validates_required_keys(self, client, mock_auth_result):
|
|
122
|
+
"""Returns 400 when keys missing."""
|
|
123
|
+
async def override_auth():
|
|
124
|
+
return mock_auth_result
|
|
125
|
+
|
|
126
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
130
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
131
|
+
|
|
132
|
+
response = client.post(
|
|
133
|
+
"/api/push/subscribe",
|
|
134
|
+
json={
|
|
135
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
136
|
+
"keys": {"p256dh": "key1"} # Missing auth
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert response.status_code == 400
|
|
141
|
+
assert "missing" in response.json()["detail"].lower()
|
|
142
|
+
finally:
|
|
143
|
+
app.dependency_overrides.clear()
|
|
144
|
+
|
|
145
|
+
def test_successful_subscription(self, client, mock_auth_result):
|
|
146
|
+
"""Successfully subscribes user to push notifications."""
|
|
147
|
+
async def override_auth():
|
|
148
|
+
return mock_auth_result
|
|
149
|
+
|
|
150
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
154
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
155
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
156
|
+
mock_service.return_value.add_subscription = AsyncMock(return_value=True)
|
|
157
|
+
|
|
158
|
+
response = client.post(
|
|
159
|
+
"/api/push/subscribe",
|
|
160
|
+
json={
|
|
161
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
162
|
+
"keys": {"p256dh": "key1", "auth": "key2"},
|
|
163
|
+
"device_name": "Test Device"
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert response.status_code == 200
|
|
168
|
+
data = response.json()
|
|
169
|
+
assert data["status"] == "success"
|
|
170
|
+
mock_service.return_value.add_subscription.assert_called_once_with(
|
|
171
|
+
user_email="test@example.com",
|
|
172
|
+
endpoint="https://push.example.com/endpoint",
|
|
173
|
+
keys={"p256dh": "key1", "auth": "key2"},
|
|
174
|
+
device_name="Test Device"
|
|
175
|
+
)
|
|
176
|
+
finally:
|
|
177
|
+
app.dependency_overrides.clear()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestUnsubscribe:
|
|
181
|
+
"""Tests for POST /api/push/unsubscribe."""
|
|
182
|
+
|
|
183
|
+
def test_requires_authentication(self, client):
|
|
184
|
+
"""Returns 401 when not authenticated."""
|
|
185
|
+
app.dependency_overrides.clear()
|
|
186
|
+
|
|
187
|
+
response = client.post(
|
|
188
|
+
"/api/push/unsubscribe",
|
|
189
|
+
json={"endpoint": "https://push.example.com/endpoint"}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert response.status_code == 401
|
|
193
|
+
|
|
194
|
+
def test_successful_unsubscribe(self, client, mock_auth_result):
|
|
195
|
+
"""Successfully unsubscribes from push notifications."""
|
|
196
|
+
async def override_auth():
|
|
197
|
+
return mock_auth_result
|
|
198
|
+
|
|
199
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
203
|
+
mock_service.return_value.remove_subscription = AsyncMock(return_value=True)
|
|
204
|
+
|
|
205
|
+
response = client.post(
|
|
206
|
+
"/api/push/unsubscribe",
|
|
207
|
+
json={"endpoint": "https://push.example.com/endpoint"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert response.status_code == 200
|
|
211
|
+
data = response.json()
|
|
212
|
+
assert data["status"] == "success"
|
|
213
|
+
mock_service.return_value.remove_subscription.assert_called_once()
|
|
214
|
+
finally:
|
|
215
|
+
app.dependency_overrides.clear()
|
|
216
|
+
|
|
217
|
+
def test_unsubscribe_not_found_succeeds(self, client, mock_auth_result):
|
|
218
|
+
"""Returns success even when subscription not found."""
|
|
219
|
+
async def override_auth():
|
|
220
|
+
return mock_auth_result
|
|
221
|
+
|
|
222
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
226
|
+
mock_service.return_value.remove_subscription = AsyncMock(return_value=False)
|
|
227
|
+
|
|
228
|
+
response = client.post(
|
|
229
|
+
"/api/push/unsubscribe",
|
|
230
|
+
json={"endpoint": "https://push.example.com/unknown"}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert response.status_code == 200
|
|
234
|
+
data = response.json()
|
|
235
|
+
assert data["status"] == "success"
|
|
236
|
+
finally:
|
|
237
|
+
app.dependency_overrides.clear()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestListSubscriptions:
|
|
241
|
+
"""Tests for GET /api/push/subscriptions."""
|
|
242
|
+
|
|
243
|
+
def test_requires_authentication(self, client):
|
|
244
|
+
"""Returns 401 when not authenticated."""
|
|
245
|
+
app.dependency_overrides.clear()
|
|
246
|
+
|
|
247
|
+
response = client.get("/api/push/subscriptions")
|
|
248
|
+
assert response.status_code == 401
|
|
249
|
+
|
|
250
|
+
def test_returns_user_subscriptions(self, client, mock_auth_result):
|
|
251
|
+
"""Returns list of user's subscriptions."""
|
|
252
|
+
async def override_auth():
|
|
253
|
+
return mock_auth_result
|
|
254
|
+
|
|
255
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
259
|
+
mock_service.return_value.list_subscriptions = AsyncMock(return_value=[
|
|
260
|
+
{
|
|
261
|
+
"endpoint": "https://push.example.com/endpoint1",
|
|
262
|
+
"device_name": "Device 1",
|
|
263
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
264
|
+
"last_used_at": None
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"endpoint": "https://push.example.com/endpoint2",
|
|
268
|
+
"device_name": "Device 2",
|
|
269
|
+
"created_at": "2024-01-02T00:00:00Z",
|
|
270
|
+
"last_used_at": "2024-01-03T00:00:00Z"
|
|
271
|
+
}
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
response = client.get("/api/push/subscriptions")
|
|
275
|
+
|
|
276
|
+
assert response.status_code == 200
|
|
277
|
+
data = response.json()
|
|
278
|
+
assert data["count"] == 2
|
|
279
|
+
assert len(data["subscriptions"]) == 2
|
|
280
|
+
assert data["subscriptions"][0]["device_name"] == "Device 1"
|
|
281
|
+
finally:
|
|
282
|
+
app.dependency_overrides.clear()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestTestNotification:
|
|
286
|
+
"""Tests for POST /api/push/test."""
|
|
287
|
+
|
|
288
|
+
def test_requires_admin(self, client, mock_auth_result):
|
|
289
|
+
"""Returns 403 when not admin."""
|
|
290
|
+
# Use a non-admin auth result
|
|
291
|
+
async def override_admin():
|
|
292
|
+
from fastapi import HTTPException
|
|
293
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
294
|
+
|
|
295
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
response = client.post(
|
|
299
|
+
"/api/push/test",
|
|
300
|
+
json={"title": "Test", "body": "Test message"}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
assert response.status_code == 403
|
|
304
|
+
finally:
|
|
305
|
+
app.dependency_overrides.clear()
|
|
306
|
+
|
|
307
|
+
def test_returns_503_when_disabled(self, client, mock_admin_auth_result):
|
|
308
|
+
"""Returns 503 when push notifications disabled."""
|
|
309
|
+
async def override_admin():
|
|
310
|
+
return mock_admin_auth_result
|
|
311
|
+
|
|
312
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
316
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
317
|
+
|
|
318
|
+
response = client.post(
|
|
319
|
+
"/api/push/test",
|
|
320
|
+
json={"title": "Test", "body": "Test message"}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert response.status_code == 503
|
|
324
|
+
finally:
|
|
325
|
+
app.dependency_overrides.clear()
|
|
326
|
+
|
|
327
|
+
def test_sends_test_notification(self, client, mock_admin_auth_result):
|
|
328
|
+
"""Successfully sends test notification to admin."""
|
|
329
|
+
async def override_admin():
|
|
330
|
+
return mock_admin_auth_result
|
|
331
|
+
|
|
332
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
336
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
337
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
338
|
+
mock_service.return_value.send_push = AsyncMock(return_value=2)
|
|
339
|
+
|
|
340
|
+
response = client.post(
|
|
341
|
+
"/api/push/test",
|
|
342
|
+
json={"title": "Test Title", "body": "Test Body"}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
assert response.status_code == 200
|
|
346
|
+
data = response.json()
|
|
347
|
+
assert data["status"] == "success"
|
|
348
|
+
assert data["sent_count"] == 2
|
|
349
|
+
mock_service.return_value.send_push.assert_called_once_with(
|
|
350
|
+
user_email="admin@nomadkaraoke.com",
|
|
351
|
+
title="Test Title",
|
|
352
|
+
body="Test Body",
|
|
353
|
+
url="/app/",
|
|
354
|
+
tag="test"
|
|
355
|
+
)
|
|
356
|
+
finally:
|
|
357
|
+
app.dependency_overrides.clear()
|