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.
Files changed (41) hide show
  1. backend/Dockerfile.base +1 -0
  2. backend/api/routes/admin.py +226 -3
  3. backend/api/routes/audio_search.py +4 -32
  4. backend/api/routes/file_upload.py +18 -83
  5. backend/api/routes/jobs.py +2 -2
  6. backend/api/routes/push.py +238 -0
  7. backend/api/routes/rate_limits.py +428 -0
  8. backend/api/routes/users.py +79 -19
  9. backend/config.py +25 -1
  10. backend/exceptions.py +66 -0
  11. backend/main.py +26 -1
  12. backend/models/job.py +4 -0
  13. backend/models/user.py +20 -2
  14. backend/services/email_validation_service.py +646 -0
  15. backend/services/firestore_service.py +21 -0
  16. backend/services/gce_encoding/main.py +22 -8
  17. backend/services/job_defaults_service.py +113 -0
  18. backend/services/job_manager.py +109 -13
  19. backend/services/push_notification_service.py +409 -0
  20. backend/services/rate_limit_service.py +641 -0
  21. backend/services/stripe_service.py +2 -2
  22. backend/tests/conftest.py +8 -1
  23. backend/tests/test_admin_delete_outputs.py +352 -0
  24. backend/tests/test_audio_search.py +12 -8
  25. backend/tests/test_email_validation_service.py +298 -0
  26. backend/tests/test_file_upload.py +8 -6
  27. backend/tests/test_gce_encoding_worker.py +229 -0
  28. backend/tests/test_impersonation.py +18 -3
  29. backend/tests/test_made_for_you.py +6 -4
  30. backend/tests/test_push_notification_service.py +460 -0
  31. backend/tests/test_push_routes.py +357 -0
  32. backend/tests/test_rate_limit_service.py +396 -0
  33. backend/tests/test_rate_limits_api.py +392 -0
  34. backend/tests/test_stripe_service.py +205 -0
  35. backend/workers/video_worker_orchestrator.py +42 -0
  36. karaoke_gen/instrumental_review/static/index.html +35 -9
  37. {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/METADATA +2 -1
  38. {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/RECORD +41 -26
  39. {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/WHEEL +0 -0
  40. {karaoke_gen-0.101.0.dist-info → karaoke_gen-0.105.4.dist-info}/entry_points.txt +0 -0
  41. {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()