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.
- backend/api/routes/admin.py +512 -1
- backend/api/routes/audio_search.py +17 -34
- backend/api/routes/file_upload.py +60 -84
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +11 -3
- backend/api/routes/rate_limits.py +428 -0
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +229 -247
- backend/config.py +16 -0
- backend/exceptions.py +66 -0
- backend/main.py +30 -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/email_validation_service.py +646 -0
- backend/services/firestore_service.py +27 -0
- backend/services/job_defaults_service.py +113 -0
- backend/services/job_manager.py +73 -3
- backend/services/rate_limit_service.py +641 -0
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/conftest.py +7 -1
- 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_audio_search.py +12 -8
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_email_validation_service.py +298 -0
- backend/tests/test_file_upload.py +8 -6
- 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 +2088 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_rate_limit_service.py +396 -0
- backend/tests/test_rate_limits_api.py +392 -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
- backend/workers/video_worker_orchestrator.py +26 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/RECORD +55 -33
- 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.103.1.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Emulator integration tests for made-for-you order features.
|
|
3
|
+
|
|
4
|
+
Tests with real Firestore emulator via API endpoints to verify:
|
|
5
|
+
1. Admin login token creation and verification
|
|
6
|
+
2. Made-for-you job fields are properly stored
|
|
7
|
+
|
|
8
|
+
Run with: scripts/run-emulator-tests.sh
|
|
9
|
+
"""
|
|
10
|
+
import pytest
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .conftest import emulators_running
|
|
14
|
+
|
|
15
|
+
# Skip all tests if emulators not running
|
|
16
|
+
pytestmark = pytest.mark.skipif(
|
|
17
|
+
not emulators_running(),
|
|
18
|
+
reason="GCP emulators not running. Start with: scripts/start-emulators.sh"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestAdminLoginTokenIntegration:
|
|
23
|
+
"""
|
|
24
|
+
Integration tests for admin login token feature via API.
|
|
25
|
+
|
|
26
|
+
These tests verify that admin login tokens can be verified
|
|
27
|
+
through the actual API endpoint.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def test_verify_invalid_token_returns_401(self, client):
|
|
31
|
+
"""Invalid token returns 401 unauthorized."""
|
|
32
|
+
response = client.get("/api/users/auth/verify?token=invalid-token-xyz")
|
|
33
|
+
assert response.status_code == 401
|
|
34
|
+
|
|
35
|
+
def test_verify_missing_token_returns_422(self, client):
|
|
36
|
+
"""Missing token parameter returns 422 validation error."""
|
|
37
|
+
response = client.get("/api/users/auth/verify")
|
|
38
|
+
assert response.status_code == 422
|
|
39
|
+
|
|
40
|
+
def test_verify_empty_token_returns_401(self, client):
|
|
41
|
+
"""Empty token returns 401."""
|
|
42
|
+
response = client.get("/api/users/auth/verify?token=")
|
|
43
|
+
assert response.status_code == 401
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestMadeForYouJobModel:
|
|
47
|
+
"""
|
|
48
|
+
Tests for made-for-you job model fields.
|
|
49
|
+
|
|
50
|
+
Verifies that the Job model properly supports made_for_you fields.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def test_job_model_has_made_for_you_fields(self):
|
|
54
|
+
"""Job model includes made_for_you and customer_email fields."""
|
|
55
|
+
from backend.models.job import Job, JobStatus
|
|
56
|
+
from datetime import datetime, timezone
|
|
57
|
+
|
|
58
|
+
# Create a made-for-you job directly
|
|
59
|
+
job = Job(
|
|
60
|
+
job_id="test-mfy-model",
|
|
61
|
+
status=JobStatus.PENDING,
|
|
62
|
+
created_at=datetime.now(timezone.utc),
|
|
63
|
+
updated_at=datetime.now(timezone.utc),
|
|
64
|
+
user_email="admin@nomadkaraoke.com",
|
|
65
|
+
made_for_you=True,
|
|
66
|
+
customer_email="customer@example.com",
|
|
67
|
+
customer_notes="Please make it perfect!",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert job.made_for_you is True
|
|
71
|
+
assert job.customer_email == "customer@example.com"
|
|
72
|
+
assert job.customer_notes == "Please make it perfect!"
|
|
73
|
+
|
|
74
|
+
def test_job_model_defaults_made_for_you_false(self):
|
|
75
|
+
"""Regular jobs default made_for_you to False."""
|
|
76
|
+
from backend.models.job import Job, JobStatus
|
|
77
|
+
from datetime import datetime, timezone
|
|
78
|
+
|
|
79
|
+
job = Job(
|
|
80
|
+
job_id="test-regular-model",
|
|
81
|
+
status=JobStatus.PENDING,
|
|
82
|
+
created_at=datetime.now(timezone.utc),
|
|
83
|
+
updated_at=datetime.now(timezone.utc),
|
|
84
|
+
user_email="user@example.com",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Default should be False or None
|
|
88
|
+
assert job.made_for_you in [False, None]
|
|
89
|
+
assert job.customer_email is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestOwnershipTransferLogic:
|
|
93
|
+
"""
|
|
94
|
+
Tests for ownership transfer logic (unit-style, runs in emulator context).
|
|
95
|
+
|
|
96
|
+
These tests verify the conditional logic for made-for-you ownership transfer
|
|
97
|
+
by importing the job_manager and testing its behavior directly.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def test_ownership_transfer_condition_made_for_you_true(self):
|
|
101
|
+
"""
|
|
102
|
+
Verify the condition: if made_for_you=True and customer_email exists,
|
|
103
|
+
ownership should transfer.
|
|
104
|
+
"""
|
|
105
|
+
# Replicate the exact logic from _schedule_completion_email
|
|
106
|
+
class MockJob:
|
|
107
|
+
made_for_you = True
|
|
108
|
+
customer_email = "customer@example.com"
|
|
109
|
+
user_email = "admin@nomadkaraoke.com"
|
|
110
|
+
job_id = "test-123"
|
|
111
|
+
|
|
112
|
+
job = MockJob()
|
|
113
|
+
|
|
114
|
+
# The logic being tested
|
|
115
|
+
recipient_email = job.user_email
|
|
116
|
+
should_transfer = False
|
|
117
|
+
|
|
118
|
+
if job.made_for_you and job.customer_email:
|
|
119
|
+
recipient_email = job.customer_email
|
|
120
|
+
should_transfer = True
|
|
121
|
+
|
|
122
|
+
assert should_transfer is True
|
|
123
|
+
assert recipient_email == "customer@example.com"
|
|
124
|
+
|
|
125
|
+
def test_ownership_transfer_condition_made_for_you_false(self):
|
|
126
|
+
"""
|
|
127
|
+
Verify: if made_for_you=False, no ownership transfer.
|
|
128
|
+
"""
|
|
129
|
+
class MockJob:
|
|
130
|
+
made_for_you = False
|
|
131
|
+
customer_email = "customer@example.com"
|
|
132
|
+
user_email = "user@example.com"
|
|
133
|
+
job_id = "test-456"
|
|
134
|
+
|
|
135
|
+
job = MockJob()
|
|
136
|
+
|
|
137
|
+
recipient_email = job.user_email
|
|
138
|
+
should_transfer = False
|
|
139
|
+
|
|
140
|
+
if job.made_for_you and job.customer_email:
|
|
141
|
+
recipient_email = job.customer_email
|
|
142
|
+
should_transfer = True
|
|
143
|
+
|
|
144
|
+
assert should_transfer is False
|
|
145
|
+
assert recipient_email == "user@example.com"
|
|
146
|
+
|
|
147
|
+
def test_ownership_transfer_condition_missing_customer_email(self):
|
|
148
|
+
"""
|
|
149
|
+
Verify: if made_for_you=True but no customer_email, no transfer.
|
|
150
|
+
"""
|
|
151
|
+
class MockJob:
|
|
152
|
+
made_for_you = True
|
|
153
|
+
customer_email = None
|
|
154
|
+
user_email = "admin@nomadkaraoke.com"
|
|
155
|
+
job_id = "test-789"
|
|
156
|
+
|
|
157
|
+
job = MockJob()
|
|
158
|
+
|
|
159
|
+
recipient_email = job.user_email
|
|
160
|
+
should_transfer = False
|
|
161
|
+
|
|
162
|
+
if job.made_for_you and job.customer_email:
|
|
163
|
+
recipient_email = job.customer_email
|
|
164
|
+
should_transfer = True
|
|
165
|
+
|
|
166
|
+
assert should_transfer is False
|
|
167
|
+
assert recipient_email == "admin@nomadkaraoke.com"
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for admin job files endpoint.
|
|
3
|
+
|
|
4
|
+
Tests the GET /api/admin/jobs/{job_id}/files endpoint that returns
|
|
5
|
+
all files for a job with signed download URLs.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
|
|
12
|
+
from backend.api.routes.admin import router
|
|
13
|
+
from backend.api.dependencies import require_admin
|
|
14
|
+
from backend.models.job import Job, JobStatus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Create a test app with the admin router
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
app.include_router(router, prefix="/api")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_mock_admin():
|
|
23
|
+
"""Override for require_admin dependency."""
|
|
24
|
+
from backend.api.dependencies import AuthResult, UserType
|
|
25
|
+
return AuthResult(
|
|
26
|
+
is_valid=True,
|
|
27
|
+
user_type=UserType.ADMIN,
|
|
28
|
+
remaining_uses=999,
|
|
29
|
+
message="Admin authenticated",
|
|
30
|
+
user_email="admin@example.com",
|
|
31
|
+
is_admin=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Override the require_admin dependency
|
|
36
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def client():
|
|
41
|
+
"""Create a test client."""
|
|
42
|
+
return TestClient(app)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def mock_job_with_files():
|
|
47
|
+
"""Create a mock job with comprehensive file_urls."""
|
|
48
|
+
job = Mock(spec=Job)
|
|
49
|
+
job.job_id = "test-job-123"
|
|
50
|
+
job.user_email = "user@example.com"
|
|
51
|
+
job.artist = "Test Artist"
|
|
52
|
+
job.title = "Test Song"
|
|
53
|
+
job.status = JobStatus.COMPLETE
|
|
54
|
+
job.file_urls = {
|
|
55
|
+
"input": "gs://bucket/jobs/test-job-123/input.flac",
|
|
56
|
+
"stems": {
|
|
57
|
+
"instrumental_clean": "gs://bucket/jobs/test-job-123/stems/instrumental_clean.flac",
|
|
58
|
+
"vocals": "gs://bucket/jobs/test-job-123/stems/vocals.flac",
|
|
59
|
+
},
|
|
60
|
+
"lyrics": {
|
|
61
|
+
"corrections": "gs://bucket/jobs/test-job-123/lyrics/corrections.json",
|
|
62
|
+
"lrc": "gs://bucket/jobs/test-job-123/lyrics/output.lrc",
|
|
63
|
+
},
|
|
64
|
+
"finals": {
|
|
65
|
+
"lossy_720p_mp4": "gs://bucket/jobs/test-job-123/finals/video_720p.mp4",
|
|
66
|
+
},
|
|
67
|
+
"youtube": {
|
|
68
|
+
"url": "https://youtube.com/watch?v=xyz789",
|
|
69
|
+
"video_id": "xyz789",
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
return job
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def mock_job_no_files():
|
|
77
|
+
"""Create a mock job with empty file_urls."""
|
|
78
|
+
job = Mock(spec=Job)
|
|
79
|
+
job.job_id = "test-job-empty"
|
|
80
|
+
job.user_email = "user@example.com"
|
|
81
|
+
job.artist = "Empty Artist"
|
|
82
|
+
job.title = "Empty Song"
|
|
83
|
+
job.status = JobStatus.PENDING
|
|
84
|
+
job.file_urls = {}
|
|
85
|
+
return job
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture
|
|
89
|
+
def mock_job_partial_files():
|
|
90
|
+
"""Create a mock job with only some files (partial processing)."""
|
|
91
|
+
job = Mock(spec=Job)
|
|
92
|
+
job.job_id = "test-job-partial"
|
|
93
|
+
job.user_email = "user@example.com"
|
|
94
|
+
job.artist = "Partial Artist"
|
|
95
|
+
job.title = "Partial Song"
|
|
96
|
+
job.status = JobStatus.SEPARATING_STAGE1
|
|
97
|
+
job.file_urls = {
|
|
98
|
+
"input": "gs://bucket/jobs/test-job-partial/input.flac",
|
|
99
|
+
"stems": {
|
|
100
|
+
"instrumental_clean": "gs://bucket/jobs/test-job-partial/stems/instrumental_clean.flac",
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
return job
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestGetJobFiles:
|
|
107
|
+
"""Tests for GET /api/admin/jobs/{job_id}/files endpoint."""
|
|
108
|
+
|
|
109
|
+
def test_returns_files_with_signed_urls(self, client, mock_job_with_files):
|
|
110
|
+
"""Test that endpoint returns all files with signed URLs."""
|
|
111
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
112
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
113
|
+
|
|
114
|
+
# Setup JobManager mock
|
|
115
|
+
mock_jm = Mock()
|
|
116
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
117
|
+
mock_jm_class.return_value = mock_jm
|
|
118
|
+
|
|
119
|
+
# Setup StorageService mock to return predictable signed URLs
|
|
120
|
+
mock_storage = Mock()
|
|
121
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
122
|
+
mock_storage_class.return_value = mock_storage
|
|
123
|
+
|
|
124
|
+
response = client.get(
|
|
125
|
+
"/api/admin/jobs/test-job-123/files",
|
|
126
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert response.status_code == 200
|
|
130
|
+
data = response.json()
|
|
131
|
+
assert data["job_id"] == "test-job-123"
|
|
132
|
+
assert data["artist"] == "Test Artist"
|
|
133
|
+
assert data["title"] == "Test Song"
|
|
134
|
+
assert "files" in data
|
|
135
|
+
assert data["total_files"] > 0
|
|
136
|
+
|
|
137
|
+
def test_returns_correct_file_count(self, client, mock_job_with_files):
|
|
138
|
+
"""Test that file count matches actual GCS files (excluding non-GCS entries)."""
|
|
139
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
140
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
141
|
+
|
|
142
|
+
mock_jm = Mock()
|
|
143
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
144
|
+
mock_jm_class.return_value = mock_jm
|
|
145
|
+
|
|
146
|
+
mock_storage = Mock()
|
|
147
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
148
|
+
mock_storage_class.return_value = mock_storage
|
|
149
|
+
|
|
150
|
+
response = client.get("/api/admin/jobs/test-job-123/files")
|
|
151
|
+
|
|
152
|
+
data = response.json()
|
|
153
|
+
# Should have: input, stems.instrumental_clean, stems.vocals,
|
|
154
|
+
# lyrics.corrections, lyrics.lrc, finals.lossy_720p_mp4
|
|
155
|
+
# NOT youtube.url or youtube.video_id (not GCS paths)
|
|
156
|
+
assert data["total_files"] == 6
|
|
157
|
+
|
|
158
|
+
def test_handles_nested_file_structure(self, client, mock_job_with_files):
|
|
159
|
+
"""Test that nested file_urls structure is properly traversed."""
|
|
160
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
161
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
162
|
+
|
|
163
|
+
mock_jm = Mock()
|
|
164
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
165
|
+
mock_jm_class.return_value = mock_jm
|
|
166
|
+
|
|
167
|
+
mock_storage = Mock()
|
|
168
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
169
|
+
mock_storage_class.return_value = mock_storage
|
|
170
|
+
|
|
171
|
+
response = client.get("/api/admin/jobs/test-job-123/files")
|
|
172
|
+
|
|
173
|
+
data = response.json()
|
|
174
|
+
files = data["files"]
|
|
175
|
+
|
|
176
|
+
# Check that files from different categories are present
|
|
177
|
+
categories = {f["category"] for f in files}
|
|
178
|
+
assert "input" in categories or any(f["category"] == "" and f["file_key"] == "input" for f in files)
|
|
179
|
+
assert "stems" in categories
|
|
180
|
+
assert "lyrics" in categories
|
|
181
|
+
assert "finals" in categories
|
|
182
|
+
|
|
183
|
+
def test_file_info_structure(self, client, mock_job_with_files):
|
|
184
|
+
"""Test that each file has correct info structure."""
|
|
185
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
186
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
187
|
+
|
|
188
|
+
mock_jm = Mock()
|
|
189
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
190
|
+
mock_jm_class.return_value = mock_jm
|
|
191
|
+
|
|
192
|
+
mock_storage = Mock()
|
|
193
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
194
|
+
mock_storage_class.return_value = mock_storage
|
|
195
|
+
|
|
196
|
+
response = client.get("/api/admin/jobs/test-job-123/files")
|
|
197
|
+
|
|
198
|
+
data = response.json()
|
|
199
|
+
files = data["files"]
|
|
200
|
+
|
|
201
|
+
# Every file should have these fields
|
|
202
|
+
for file_info in files:
|
|
203
|
+
assert "name" in file_info
|
|
204
|
+
assert "path" in file_info
|
|
205
|
+
assert "download_url" in file_info
|
|
206
|
+
assert "category" in file_info
|
|
207
|
+
assert "file_key" in file_info
|
|
208
|
+
# download_url should be a signed URL
|
|
209
|
+
assert file_info["download_url"].startswith("https://signed.url/")
|
|
210
|
+
|
|
211
|
+
def test_returns_404_when_job_not_found(self, client):
|
|
212
|
+
"""Test 404 when job doesn't exist."""
|
|
213
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class:
|
|
214
|
+
mock_jm = Mock()
|
|
215
|
+
mock_jm.get_job.return_value = None
|
|
216
|
+
mock_jm_class.return_value = mock_jm
|
|
217
|
+
|
|
218
|
+
response = client.get(
|
|
219
|
+
"/api/admin/jobs/nonexistent-job/files",
|
|
220
|
+
headers={"Authorization": "Bearer admin-token"}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert response.status_code == 404
|
|
224
|
+
assert "not found" in response.json()["detail"].lower()
|
|
225
|
+
|
|
226
|
+
def test_returns_empty_files_for_new_job(self, client, mock_job_no_files):
|
|
227
|
+
"""Test that new jobs with no files return empty list."""
|
|
228
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
229
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
230
|
+
|
|
231
|
+
mock_jm = Mock()
|
|
232
|
+
mock_jm.get_job.return_value = mock_job_no_files
|
|
233
|
+
mock_jm_class.return_value = mock_jm
|
|
234
|
+
|
|
235
|
+
mock_storage = Mock()
|
|
236
|
+
mock_storage_class.return_value = mock_storage
|
|
237
|
+
|
|
238
|
+
response = client.get("/api/admin/jobs/test-job-empty/files")
|
|
239
|
+
|
|
240
|
+
assert response.status_code == 200
|
|
241
|
+
data = response.json()
|
|
242
|
+
assert data["job_id"] == "test-job-empty"
|
|
243
|
+
assert data["files"] == []
|
|
244
|
+
assert data["total_files"] == 0
|
|
245
|
+
|
|
246
|
+
def test_handles_partial_processing(self, client, mock_job_partial_files):
|
|
247
|
+
"""Test job with only some files processed."""
|
|
248
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
249
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
250
|
+
|
|
251
|
+
mock_jm = Mock()
|
|
252
|
+
mock_jm.get_job.return_value = mock_job_partial_files
|
|
253
|
+
mock_jm_class.return_value = mock_jm
|
|
254
|
+
|
|
255
|
+
mock_storage = Mock()
|
|
256
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
257
|
+
mock_storage_class.return_value = mock_storage
|
|
258
|
+
|
|
259
|
+
response = client.get("/api/admin/jobs/test-job-partial/files")
|
|
260
|
+
|
|
261
|
+
assert response.status_code == 200
|
|
262
|
+
data = response.json()
|
|
263
|
+
# Should have input + stems.instrumental_clean = 2 files
|
|
264
|
+
assert data["total_files"] == 2
|
|
265
|
+
|
|
266
|
+
def test_skips_non_gcs_urls(self, client, mock_job_with_files):
|
|
267
|
+
"""Test that non-GCS URLs (like youtube URLs) are skipped."""
|
|
268
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
269
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
270
|
+
|
|
271
|
+
mock_jm = Mock()
|
|
272
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
273
|
+
mock_jm_class.return_value = mock_jm
|
|
274
|
+
|
|
275
|
+
mock_storage = Mock()
|
|
276
|
+
mock_storage.generate_signed_url.side_effect = lambda path, **kwargs: f"https://signed.url/{path}"
|
|
277
|
+
mock_storage_class.return_value = mock_storage
|
|
278
|
+
|
|
279
|
+
response = client.get("/api/admin/jobs/test-job-123/files")
|
|
280
|
+
|
|
281
|
+
data = response.json()
|
|
282
|
+
files = data["files"]
|
|
283
|
+
|
|
284
|
+
# Ensure no youtube URLs or video IDs are in the files
|
|
285
|
+
for file_info in files:
|
|
286
|
+
assert "youtube.com" not in file_info["path"]
|
|
287
|
+
assert file_info["path"].startswith("gs://")
|
|
288
|
+
|
|
289
|
+
def test_signed_url_expiration(self, client, mock_job_with_files):
|
|
290
|
+
"""Test that signed URLs are generated with appropriate expiration."""
|
|
291
|
+
with patch('backend.api.routes.admin.JobManager') as mock_jm_class, \
|
|
292
|
+
patch('backend.api.routes.admin.StorageService') as mock_storage_class:
|
|
293
|
+
|
|
294
|
+
mock_jm = Mock()
|
|
295
|
+
mock_jm.get_job.return_value = mock_job_with_files
|
|
296
|
+
mock_jm_class.return_value = mock_jm
|
|
297
|
+
|
|
298
|
+
mock_storage = Mock()
|
|
299
|
+
mock_storage.generate_signed_url.return_value = "https://signed.url/test"
|
|
300
|
+
mock_storage_class.return_value = mock_storage
|
|
301
|
+
|
|
302
|
+
client.get("/api/admin/jobs/test-job-123/files")
|
|
303
|
+
|
|
304
|
+
# Verify signed URLs were requested with expiration (default 120 minutes)
|
|
305
|
+
assert mock_storage.generate_signed_url.called
|
|
306
|
+
# Check that expiration_minutes was passed (could be any reasonable value)
|
|
307
|
+
call_kwargs = mock_storage.generate_signed_url.call_args_list[0][1]
|
|
308
|
+
if "expiration_minutes" in call_kwargs:
|
|
309
|
+
assert call_kwargs["expiration_minutes"] >= 60
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestGetJobFilesAuthorization:
|
|
313
|
+
"""Tests for authorization on the files endpoint."""
|
|
314
|
+
|
|
315
|
+
def test_requires_admin_access(self, client, mock_job_with_files):
|
|
316
|
+
"""Test that non-admin users cannot access the endpoint."""
|
|
317
|
+
# Reset the dependency override to test auth
|
|
318
|
+
original_override = app.dependency_overrides.get(require_admin)
|
|
319
|
+
|
|
320
|
+
def get_non_admin():
|
|
321
|
+
from fastapi import HTTPException
|
|
322
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
323
|
+
|
|
324
|
+
app.dependency_overrides[require_admin] = get_non_admin
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
response = client.get(
|
|
328
|
+
"/api/admin/jobs/test-job-123/files",
|
|
329
|
+
headers={"Authorization": "Bearer user-token"}
|
|
330
|
+
)
|
|
331
|
+
assert response.status_code == 403
|
|
332
|
+
finally:
|
|
333
|
+
# Restore the original override
|
|
334
|
+
if original_override:
|
|
335
|
+
app.dependency_overrides[require_admin] = original_override
|
|
336
|
+
else:
|
|
337
|
+
app.dependency_overrides[require_admin] = get_mock_admin
|