karaoke-gen 0.90.1__py3-none-any.whl → 0.99.3__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/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +835 -0
- backend/api/routes/audio_search.py +913 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2112 -0
- backend/api/routes/health.py +409 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1629 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1513 -0
- backend/config.py +172 -0
- backend/main.py +157 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +502 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +853 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/langfuse_preloader.py +98 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +371 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +109 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +356 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +283 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_spacy_preloader.py +119 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/utils/test_data.py +27 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +535 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for service layer.
|
|
3
|
+
|
|
4
|
+
Tests AuthService, StorageService, WorkerService, and FirestoreService
|
|
5
|
+
without requiring actual cloud resources.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import Mock, MagicMock, AsyncMock, patch, call
|
|
9
|
+
from datetime import datetime, UTC
|
|
10
|
+
|
|
11
|
+
# Mock Google Cloud before imports
|
|
12
|
+
import sys
|
|
13
|
+
sys.modules['google.cloud.firestore'] = MagicMock()
|
|
14
|
+
sys.modules['google.cloud.storage'] = MagicMock()
|
|
15
|
+
sys.modules['google.cloud.tasks_v2'] = MagicMock()
|
|
16
|
+
|
|
17
|
+
from backend.services.auth_service import AuthService, UserType
|
|
18
|
+
from backend.services.storage_service import StorageService
|
|
19
|
+
from backend.services.worker_service import WorkerService, get_worker_service
|
|
20
|
+
from backend.services.firestore_service import FirestoreService
|
|
21
|
+
from backend.models.job import Job, JobStatus
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAuthService:
|
|
25
|
+
"""Test AuthService token validation and management."""
|
|
26
|
+
|
|
27
|
+
def test_validate_admin_token(self):
|
|
28
|
+
"""Test validating hardcoded admin tokens."""
|
|
29
|
+
with patch('backend.services.auth_service.get_settings') as mock_settings:
|
|
30
|
+
mock_settings.return_value.admin_tokens = "admin123,secret456"
|
|
31
|
+
|
|
32
|
+
auth_service = AuthService()
|
|
33
|
+
|
|
34
|
+
# Valid admin token - returns (is_valid, user_type, usage_count, token)
|
|
35
|
+
is_valid, user_type, usage_count, token = auth_service.validate_token('admin123')
|
|
36
|
+
assert is_valid is True
|
|
37
|
+
assert user_type == UserType.ADMIN
|
|
38
|
+
|
|
39
|
+
# Another valid admin token
|
|
40
|
+
is_valid, user_type, _, _ = auth_service.validate_token('secret456')
|
|
41
|
+
assert is_valid is True
|
|
42
|
+
assert user_type == UserType.ADMIN
|
|
43
|
+
|
|
44
|
+
def test_validate_invalid_token(self):
|
|
45
|
+
"""Test rejecting invalid tokens."""
|
|
46
|
+
with patch('backend.services.auth_service.FirestoreService') as mock_fs:
|
|
47
|
+
mock_fs_instance = Mock()
|
|
48
|
+
mock_fs.return_value = mock_fs_instance
|
|
49
|
+
mock_fs_instance.get_token.return_value = None # Token not in DB
|
|
50
|
+
|
|
51
|
+
with patch('backend.services.auth_service.get_settings') as mock_settings:
|
|
52
|
+
mock_settings.return_value.admin_tokens = "admin123"
|
|
53
|
+
|
|
54
|
+
auth_service = AuthService()
|
|
55
|
+
is_valid, _, _, _ = auth_service.validate_token('invalid_token')
|
|
56
|
+
|
|
57
|
+
assert is_valid is False
|
|
58
|
+
|
|
59
|
+
def test_validate_token_from_firestore(self):
|
|
60
|
+
"""Test validating tokens stored in Firestore."""
|
|
61
|
+
with patch('backend.services.auth_service.FirestoreService') as mock_fs:
|
|
62
|
+
mock_fs_instance = Mock()
|
|
63
|
+
mock_fs.return_value = mock_fs_instance
|
|
64
|
+
|
|
65
|
+
# Mock Firestore returning a token
|
|
66
|
+
mock_fs_instance.get_token.return_value = {
|
|
67
|
+
'token': 'db_token123',
|
|
68
|
+
'type': 'unlimited',
|
|
69
|
+
'valid': True,
|
|
70
|
+
'usage_count': 5,
|
|
71
|
+
'created_at': datetime.now(UTC).isoformat()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
with patch('backend.services.auth_service.get_settings') as mock_settings:
|
|
75
|
+
mock_settings.return_value.admin_tokens = ""
|
|
76
|
+
|
|
77
|
+
auth_service = AuthService()
|
|
78
|
+
is_valid, user_type, usage_count, token = auth_service.validate_token('db_token123')
|
|
79
|
+
|
|
80
|
+
assert is_valid is True
|
|
81
|
+
assert user_type == UserType.UNLIMITED
|
|
82
|
+
# usage_count can be -1 for unlimited tokens
|
|
83
|
+
assert isinstance(usage_count, int)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestStorageService:
|
|
87
|
+
"""Test StorageService GCS operations."""
|
|
88
|
+
|
|
89
|
+
def test_upload_file(self):
|
|
90
|
+
"""Test uploading a file to GCS."""
|
|
91
|
+
with patch('backend.services.storage_service.storage') as mock_storage:
|
|
92
|
+
mock_client = MagicMock()
|
|
93
|
+
mock_storage.Client.return_value = mock_client
|
|
94
|
+
|
|
95
|
+
mock_bucket = MagicMock()
|
|
96
|
+
mock_client.bucket.return_value = mock_bucket
|
|
97
|
+
|
|
98
|
+
mock_blob = MagicMock()
|
|
99
|
+
mock_bucket.blob.return_value = mock_blob
|
|
100
|
+
|
|
101
|
+
storage_service = StorageService()
|
|
102
|
+
|
|
103
|
+
# Upload file - correct parameter names
|
|
104
|
+
result = storage_service.upload_file(
|
|
105
|
+
local_path="/tmp/test.flac",
|
|
106
|
+
destination_path="uploads/test123/test.flac"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Verify blob was created and uploaded
|
|
110
|
+
mock_bucket.blob.assert_called_once_with("uploads/test123/test.flac")
|
|
111
|
+
mock_blob.upload_from_filename.assert_called_once_with("/tmp/test.flac")
|
|
112
|
+
# Result should be the destination path
|
|
113
|
+
assert result == "uploads/test123/test.flac"
|
|
114
|
+
|
|
115
|
+
def test_download_file(self):
|
|
116
|
+
"""Test downloading a file from GCS."""
|
|
117
|
+
with patch('backend.services.storage_service.storage') as mock_storage:
|
|
118
|
+
mock_client = MagicMock()
|
|
119
|
+
mock_storage.Client.return_value = mock_client
|
|
120
|
+
|
|
121
|
+
mock_bucket = MagicMock()
|
|
122
|
+
mock_client.bucket.return_value = mock_bucket
|
|
123
|
+
|
|
124
|
+
mock_blob = MagicMock()
|
|
125
|
+
mock_bucket.blob.return_value = mock_blob
|
|
126
|
+
|
|
127
|
+
storage_service = StorageService()
|
|
128
|
+
|
|
129
|
+
# Download file - correct parameter names
|
|
130
|
+
storage_service.download_file(
|
|
131
|
+
source_path="uploads/test123/test.flac",
|
|
132
|
+
destination_path="/tmp/downloaded.flac"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Verify download was called
|
|
136
|
+
mock_blob.download_to_filename.assert_called_once_with("/tmp/downloaded.flac")
|
|
137
|
+
|
|
138
|
+
def test_delete_file(self):
|
|
139
|
+
"""Test deleting a file from GCS."""
|
|
140
|
+
with patch('backend.services.storage_service.storage') as mock_storage:
|
|
141
|
+
mock_client = MagicMock()
|
|
142
|
+
mock_storage.Client.return_value = mock_client
|
|
143
|
+
|
|
144
|
+
mock_bucket = MagicMock()
|
|
145
|
+
mock_client.bucket.return_value = mock_bucket
|
|
146
|
+
|
|
147
|
+
mock_blob = MagicMock()
|
|
148
|
+
mock_bucket.blob.return_value = mock_blob
|
|
149
|
+
|
|
150
|
+
storage_service = StorageService()
|
|
151
|
+
|
|
152
|
+
# Delete file
|
|
153
|
+
storage_service.delete_file("uploads/test123/test.flac")
|
|
154
|
+
|
|
155
|
+
# Verify delete was called
|
|
156
|
+
mock_blob.delete.assert_called_once()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestWorkerService:
|
|
160
|
+
"""Test WorkerService internal HTTP calls."""
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_trigger_audio_worker(self):
|
|
164
|
+
"""Test triggering audio worker via internal HTTP."""
|
|
165
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
166
|
+
mock_client = AsyncMock()
|
|
167
|
+
mock_client_cls.return_value.__aenter__.return_value = mock_client
|
|
168
|
+
|
|
169
|
+
mock_response = Mock()
|
|
170
|
+
mock_response.status_code = 200
|
|
171
|
+
mock_client.post.return_value = mock_response
|
|
172
|
+
|
|
173
|
+
worker_service = get_worker_service()
|
|
174
|
+
|
|
175
|
+
await worker_service.trigger_audio_worker("test123")
|
|
176
|
+
|
|
177
|
+
# Verify HTTP POST was made
|
|
178
|
+
mock_client.post.assert_called_once()
|
|
179
|
+
call_args = mock_client.post.call_args
|
|
180
|
+
assert "/api/internal/workers/audio" in str(call_args)
|
|
181
|
+
|
|
182
|
+
@pytest.mark.asyncio
|
|
183
|
+
async def test_trigger_lyrics_worker(self):
|
|
184
|
+
"""Test triggering lyrics worker via internal HTTP."""
|
|
185
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
186
|
+
mock_client = AsyncMock()
|
|
187
|
+
mock_client_cls.return_value.__aenter__.return_value = mock_client
|
|
188
|
+
|
|
189
|
+
mock_response = Mock()
|
|
190
|
+
mock_response.status_code = 200
|
|
191
|
+
mock_client.post.return_value = mock_response
|
|
192
|
+
|
|
193
|
+
worker_service = get_worker_service()
|
|
194
|
+
|
|
195
|
+
await worker_service.trigger_lyrics_worker("test123")
|
|
196
|
+
|
|
197
|
+
# Verify HTTP POST was made
|
|
198
|
+
mock_client.post.assert_called_once()
|
|
199
|
+
call_args = mock_client.post.call_args
|
|
200
|
+
assert "/api/internal/workers/lyrics" in str(call_args)
|
|
201
|
+
|
|
202
|
+
@pytest.mark.asyncio
|
|
203
|
+
async def test_trigger_screens_worker(self):
|
|
204
|
+
"""Test triggering screens worker via internal HTTP."""
|
|
205
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
206
|
+
mock_client = AsyncMock()
|
|
207
|
+
mock_client_cls.return_value.__aenter__.return_value = mock_client
|
|
208
|
+
|
|
209
|
+
mock_response = Mock()
|
|
210
|
+
mock_response.status_code = 200
|
|
211
|
+
mock_client.post.return_value = mock_response
|
|
212
|
+
|
|
213
|
+
worker_service = get_worker_service()
|
|
214
|
+
|
|
215
|
+
await worker_service.trigger_screens_worker("test123")
|
|
216
|
+
|
|
217
|
+
# Verify HTTP POST was made
|
|
218
|
+
mock_client.post.assert_called_once()
|
|
219
|
+
call_args = mock_client.post.call_args
|
|
220
|
+
assert "/api/internal/workers/screens" in str(call_args)
|
|
221
|
+
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_trigger_video_worker(self):
|
|
224
|
+
"""Test triggering video worker via internal HTTP."""
|
|
225
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
226
|
+
mock_client = AsyncMock()
|
|
227
|
+
mock_client_cls.return_value.__aenter__.return_value = mock_client
|
|
228
|
+
|
|
229
|
+
mock_response = Mock()
|
|
230
|
+
mock_response.status_code = 200
|
|
231
|
+
mock_client.post.return_value = mock_response
|
|
232
|
+
|
|
233
|
+
worker_service = get_worker_service()
|
|
234
|
+
|
|
235
|
+
await worker_service.trigger_video_worker("test123")
|
|
236
|
+
|
|
237
|
+
# Verify HTTP POST was made
|
|
238
|
+
mock_client.post.assert_called_once()
|
|
239
|
+
call_args = mock_client.post.call_args
|
|
240
|
+
assert "/api/internal/workers/video" in str(call_args)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestWorkerServiceCloudTasks:
|
|
244
|
+
"""Test WorkerService Cloud Tasks integration."""
|
|
245
|
+
|
|
246
|
+
def test_should_use_cloud_tasks_default_false(self):
|
|
247
|
+
"""Test that Cloud Tasks is disabled by default."""
|
|
248
|
+
from backend.services.worker_service import WorkerService, reset_worker_service
|
|
249
|
+
reset_worker_service()
|
|
250
|
+
|
|
251
|
+
with patch('backend.services.worker_service.os.getenv') as mock_getenv:
|
|
252
|
+
mock_getenv.side_effect = lambda k, d=None: {
|
|
253
|
+
'PORT': '8000',
|
|
254
|
+
}.get(k, d)
|
|
255
|
+
|
|
256
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
257
|
+
mock_settings.return_value.admin_tokens = None
|
|
258
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
259
|
+
mock_settings.return_value.enable_cloud_tasks = False
|
|
260
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
261
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
262
|
+
|
|
263
|
+
service = WorkerService()
|
|
264
|
+
assert service._use_cloud_tasks is False
|
|
265
|
+
|
|
266
|
+
def test_should_use_cloud_tasks_enabled(self):
|
|
267
|
+
"""Test that Cloud Tasks can be enabled via settings."""
|
|
268
|
+
from backend.services.worker_service import WorkerService, reset_worker_service
|
|
269
|
+
reset_worker_service()
|
|
270
|
+
|
|
271
|
+
with patch('backend.services.worker_service.os.getenv') as mock_getenv:
|
|
272
|
+
mock_getenv.side_effect = lambda k, d=None: {
|
|
273
|
+
'PORT': '8000',
|
|
274
|
+
}.get(k, d)
|
|
275
|
+
|
|
276
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
277
|
+
mock_settings.return_value.admin_tokens = None
|
|
278
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
279
|
+
mock_settings.return_value.enable_cloud_tasks = True
|
|
280
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
281
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
282
|
+
|
|
283
|
+
service = WorkerService()
|
|
284
|
+
assert service._use_cloud_tasks is True
|
|
285
|
+
|
|
286
|
+
def test_worker_queues_mapping(self):
|
|
287
|
+
"""Test that all worker types have queue mappings."""
|
|
288
|
+
from backend.services.worker_service import WORKER_QUEUES
|
|
289
|
+
|
|
290
|
+
# Verify all expected workers have queues
|
|
291
|
+
assert "audio" in WORKER_QUEUES
|
|
292
|
+
assert "lyrics" in WORKER_QUEUES
|
|
293
|
+
assert "screens" in WORKER_QUEUES
|
|
294
|
+
assert "render-video" in WORKER_QUEUES
|
|
295
|
+
assert "video" in WORKER_QUEUES
|
|
296
|
+
|
|
297
|
+
# Verify queue names are correct
|
|
298
|
+
assert WORKER_QUEUES["audio"] == "audio-worker-queue"
|
|
299
|
+
assert WORKER_QUEUES["lyrics"] == "lyrics-worker-queue"
|
|
300
|
+
assert WORKER_QUEUES["screens"] == "screens-worker-queue"
|
|
301
|
+
assert WORKER_QUEUES["render-video"] == "render-worker-queue"
|
|
302
|
+
assert WORKER_QUEUES["video"] == "video-worker-queue"
|
|
303
|
+
|
|
304
|
+
@pytest.mark.asyncio
|
|
305
|
+
async def test_trigger_worker_uses_http_when_cloud_tasks_disabled(self):
|
|
306
|
+
"""Test that trigger_worker uses HTTP when Cloud Tasks is disabled."""
|
|
307
|
+
from backend.services.worker_service import WorkerService, reset_worker_service
|
|
308
|
+
reset_worker_service()
|
|
309
|
+
|
|
310
|
+
with patch('backend.services.worker_service.os.getenv') as mock_getenv:
|
|
311
|
+
mock_getenv.side_effect = lambda k, d=None: {
|
|
312
|
+
'PORT': '8000',
|
|
313
|
+
}.get(k, d)
|
|
314
|
+
|
|
315
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
316
|
+
mock_settings.return_value.admin_tokens = "test-token"
|
|
317
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
318
|
+
mock_settings.return_value.enable_cloud_tasks = False
|
|
319
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
320
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
321
|
+
|
|
322
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
323
|
+
mock_client = AsyncMock()
|
|
324
|
+
mock_client_cls.return_value.__aenter__.return_value = mock_client
|
|
325
|
+
|
|
326
|
+
mock_response = Mock()
|
|
327
|
+
mock_response.status_code = 200
|
|
328
|
+
mock_client.post.return_value = mock_response
|
|
329
|
+
|
|
330
|
+
service = WorkerService()
|
|
331
|
+
assert service._use_cloud_tasks is False
|
|
332
|
+
|
|
333
|
+
result = await service.trigger_worker("audio", "test-job-123")
|
|
334
|
+
|
|
335
|
+
# Verify HTTP was used
|
|
336
|
+
assert result is True
|
|
337
|
+
mock_client.post.assert_called_once()
|
|
338
|
+
|
|
339
|
+
@pytest.mark.asyncio
|
|
340
|
+
async def test_trigger_worker_uses_cloud_tasks_when_enabled(self):
|
|
341
|
+
"""Test that trigger_worker uses Cloud Tasks when enabled."""
|
|
342
|
+
from backend.services.worker_service import WorkerService, reset_worker_service
|
|
343
|
+
reset_worker_service()
|
|
344
|
+
|
|
345
|
+
with patch('backend.services.worker_service.os.getenv') as mock_getenv:
|
|
346
|
+
mock_getenv.side_effect = lambda k, d=None: {
|
|
347
|
+
'CLOUD_RUN_SERVICE_URL': 'https://api.example.com',
|
|
348
|
+
}.get(k, d)
|
|
349
|
+
|
|
350
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
351
|
+
mock_settings.return_value.admin_tokens = "test-token"
|
|
352
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
353
|
+
mock_settings.return_value.enable_cloud_tasks = True
|
|
354
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
355
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
356
|
+
|
|
357
|
+
# Mock Cloud Tasks client
|
|
358
|
+
mock_tasks_client = MagicMock()
|
|
359
|
+
mock_tasks_client.queue_path.return_value = "projects/test-project/locations/us-central1/queues/audio-worker-queue"
|
|
360
|
+
mock_task_response = MagicMock()
|
|
361
|
+
mock_task_response.name = "projects/test-project/locations/us-central1/queues/audio-worker-queue/tasks/abc123"
|
|
362
|
+
mock_tasks_client.create_task.return_value = mock_task_response
|
|
363
|
+
|
|
364
|
+
# Mock the google.cloud.tasks_v2 module which is imported inside the method
|
|
365
|
+
mock_tasks_module = MagicMock()
|
|
366
|
+
mock_tasks_module.CloudTasksClient.return_value = mock_tasks_client
|
|
367
|
+
mock_tasks_module.HttpMethod.POST = "POST"
|
|
368
|
+
|
|
369
|
+
with patch.dict('sys.modules', {'google.cloud.tasks_v2': mock_tasks_module}):
|
|
370
|
+
with patch.dict(sys.modules, {'google.cloud': MagicMock()}):
|
|
371
|
+
service = WorkerService()
|
|
372
|
+
service._tasks_client = mock_tasks_client # Inject mocked client
|
|
373
|
+
|
|
374
|
+
result = await service.trigger_worker("audio", "test-job-123")
|
|
375
|
+
|
|
376
|
+
# Verify Cloud Tasks was used
|
|
377
|
+
assert result is True
|
|
378
|
+
mock_tasks_client.create_task.assert_called_once()
|
|
379
|
+
|
|
380
|
+
# Verify task payload
|
|
381
|
+
call_args = mock_tasks_client.create_task.call_args
|
|
382
|
+
assert call_args is not None
|
|
383
|
+
|
|
384
|
+
@pytest.mark.asyncio
|
|
385
|
+
async def test_trigger_worker_returns_false_for_unknown_worker_type(self):
|
|
386
|
+
"""Test that trigger_worker returns False for unknown worker types."""
|
|
387
|
+
from backend.services.worker_service import WorkerService, reset_worker_service
|
|
388
|
+
reset_worker_service()
|
|
389
|
+
|
|
390
|
+
with patch('backend.services.worker_service.os.getenv') as mock_getenv:
|
|
391
|
+
mock_getenv.side_effect = lambda k, d=None: {
|
|
392
|
+
'CLOUD_RUN_SERVICE_URL': 'https://api.example.com',
|
|
393
|
+
}.get(k, d)
|
|
394
|
+
|
|
395
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
396
|
+
mock_settings.return_value.admin_tokens = "test-token"
|
|
397
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
398
|
+
mock_settings.return_value.enable_cloud_tasks = True
|
|
399
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
400
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
401
|
+
|
|
402
|
+
service = WorkerService()
|
|
403
|
+
|
|
404
|
+
result = await service.trigger_worker("unknown-worker", "test-job-123")
|
|
405
|
+
|
|
406
|
+
assert result is False
|
|
407
|
+
|
|
408
|
+
def test_reset_worker_service(self):
|
|
409
|
+
"""Test that reset_worker_service resets the singleton."""
|
|
410
|
+
from backend.services.worker_service import get_worker_service, reset_worker_service
|
|
411
|
+
|
|
412
|
+
with patch('backend.services.worker_service.get_settings') as mock_settings:
|
|
413
|
+
mock_settings.return_value.admin_tokens = None
|
|
414
|
+
mock_settings.return_value.google_cloud_project = "test-project"
|
|
415
|
+
mock_settings.return_value.enable_cloud_tasks = False
|
|
416
|
+
mock_settings.return_value.gcp_region = "us-central1"
|
|
417
|
+
mock_settings.return_value.use_cloud_run_jobs_for_video = False
|
|
418
|
+
|
|
419
|
+
service1 = get_worker_service()
|
|
420
|
+
reset_worker_service()
|
|
421
|
+
service2 = get_worker_service()
|
|
422
|
+
|
|
423
|
+
# After reset, should be different instances
|
|
424
|
+
assert service1 is not service2
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class TestFirestoreService:
|
|
428
|
+
"""Test FirestoreService database operations."""
|
|
429
|
+
|
|
430
|
+
def test_create_job(self):
|
|
431
|
+
"""Test creating a job document in Firestore."""
|
|
432
|
+
with patch('backend.services.firestore_service.firestore') as mock_firestore:
|
|
433
|
+
mock_client = MagicMock()
|
|
434
|
+
mock_firestore.Client.return_value = mock_client
|
|
435
|
+
|
|
436
|
+
mock_collection = MagicMock()
|
|
437
|
+
mock_client.collection.return_value = mock_collection
|
|
438
|
+
|
|
439
|
+
mock_doc_ref = MagicMock()
|
|
440
|
+
mock_collection.document.return_value = mock_doc_ref
|
|
441
|
+
|
|
442
|
+
firestore_service = FirestoreService()
|
|
443
|
+
|
|
444
|
+
job = Job(
|
|
445
|
+
job_id="test123",
|
|
446
|
+
status=JobStatus.PENDING,
|
|
447
|
+
created_at=datetime.now(UTC),
|
|
448
|
+
updated_at=datetime.now(UTC)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
firestore_service.create_job(job) # Returns None
|
|
452
|
+
|
|
453
|
+
# Verify document was set
|
|
454
|
+
mock_doc_ref.set.assert_called_once()
|
|
455
|
+
|
|
456
|
+
def test_get_job(self):
|
|
457
|
+
"""Test fetching a job from Firestore."""
|
|
458
|
+
with patch('backend.services.firestore_service.firestore') as mock_firestore:
|
|
459
|
+
mock_client = MagicMock()
|
|
460
|
+
mock_firestore.Client.return_value = mock_client
|
|
461
|
+
|
|
462
|
+
mock_collection = MagicMock()
|
|
463
|
+
mock_client.collection.return_value = mock_collection
|
|
464
|
+
|
|
465
|
+
mock_doc_ref = MagicMock()
|
|
466
|
+
mock_collection.document.return_value = mock_doc_ref
|
|
467
|
+
|
|
468
|
+
mock_doc = MagicMock()
|
|
469
|
+
mock_doc.exists = True
|
|
470
|
+
mock_doc.to_dict.return_value = {
|
|
471
|
+
'job_id': 'test123',
|
|
472
|
+
'status': 'pending',
|
|
473
|
+
'progress': 0,
|
|
474
|
+
'created_at': datetime.now(UTC).isoformat(),
|
|
475
|
+
'updated_at': datetime.now(UTC).isoformat()
|
|
476
|
+
}
|
|
477
|
+
mock_doc_ref.get.return_value = mock_doc
|
|
478
|
+
|
|
479
|
+
firestore_service = FirestoreService()
|
|
480
|
+
|
|
481
|
+
job = firestore_service.get_job("test123")
|
|
482
|
+
|
|
483
|
+
# Verify document was fetched
|
|
484
|
+
mock_doc_ref.get.assert_called_once()
|
|
485
|
+
assert job is not None
|
|
486
|
+
assert job.job_id == "test123"
|
|
487
|
+
|
|
488
|
+
def test_get_nonexistent_job(self):
|
|
489
|
+
"""Test fetching a job that doesn't exist."""
|
|
490
|
+
with patch('backend.services.firestore_service.firestore') as mock_firestore:
|
|
491
|
+
mock_client = MagicMock()
|
|
492
|
+
mock_firestore.Client.return_value = mock_client
|
|
493
|
+
|
|
494
|
+
mock_collection = MagicMock()
|
|
495
|
+
mock_client.collection.return_value = mock_collection
|
|
496
|
+
|
|
497
|
+
mock_doc_ref = MagicMock()
|
|
498
|
+
mock_collection.document.return_value = mock_doc_ref
|
|
499
|
+
|
|
500
|
+
mock_doc = MagicMock()
|
|
501
|
+
mock_doc.exists = False
|
|
502
|
+
mock_doc_ref.get.return_value = mock_doc
|
|
503
|
+
|
|
504
|
+
firestore_service = FirestoreService()
|
|
505
|
+
|
|
506
|
+
job = firestore_service.get_job("nonexistent")
|
|
507
|
+
|
|
508
|
+
assert job is None
|
|
509
|
+
|
|
510
|
+
def test_update_job(self):
|
|
511
|
+
"""Test updating a job in Firestore."""
|
|
512
|
+
with patch('backend.services.firestore_service.firestore') as mock_firestore:
|
|
513
|
+
mock_client = MagicMock()
|
|
514
|
+
mock_firestore.Client.return_value = mock_client
|
|
515
|
+
|
|
516
|
+
mock_collection = MagicMock()
|
|
517
|
+
mock_client.collection.return_value = mock_collection
|
|
518
|
+
|
|
519
|
+
mock_doc_ref = MagicMock()
|
|
520
|
+
mock_collection.document.return_value = mock_doc_ref
|
|
521
|
+
|
|
522
|
+
firestore_service = FirestoreService()
|
|
523
|
+
|
|
524
|
+
updates = {
|
|
525
|
+
'status': JobStatus.SEPARATING_STAGE1,
|
|
526
|
+
'progress': 25
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
firestore_service.update_job("test123", updates)
|
|
530
|
+
|
|
531
|
+
# Verify update was called
|
|
532
|
+
mock_doc_ref.update.assert_called_once()
|
|
533
|
+
|
|
534
|
+
def test_delete_job(self):
|
|
535
|
+
"""Test deleting a job from Firestore."""
|
|
536
|
+
with patch('backend.services.firestore_service.firestore') as mock_firestore:
|
|
537
|
+
mock_client = MagicMock()
|
|
538
|
+
mock_firestore.Client.return_value = mock_client
|
|
539
|
+
|
|
540
|
+
mock_collection = MagicMock()
|
|
541
|
+
mock_client.collection.return_value = mock_collection
|
|
542
|
+
|
|
543
|
+
mock_doc_ref = MagicMock()
|
|
544
|
+
mock_collection.document.return_value = mock_doc_ref
|
|
545
|
+
|
|
546
|
+
firestore_service = FirestoreService()
|
|
547
|
+
|
|
548
|
+
firestore_service.delete_job("test123")
|
|
549
|
+
|
|
550
|
+
# Verify delete was called
|
|
551
|
+
mock_doc_ref.delete.assert_called_once()
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if __name__ == "__main__":
|
|
555
|
+
pytest.main([__file__, "-v"])
|
|
556
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extended unit tests for services.
|
|
3
|
+
|
|
4
|
+
These tests provide additional coverage for service layer code.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from datetime import datetime, UTC
|
|
8
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
9
|
+
|
|
10
|
+
from backend.models.job import Job, JobStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestJobManagerExtended:
|
|
14
|
+
"""Extended tests for JobManager.
|
|
15
|
+
|
|
16
|
+
Note: These tests verify the module structure and basic behavior.
|
|
17
|
+
Full JobManager testing is in test_job_manager.py.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def test_job_manager_module_imports(self):
|
|
21
|
+
"""Test JobManager module can be imported."""
|
|
22
|
+
from backend.services import job_manager
|
|
23
|
+
assert hasattr(job_manager, 'JobManager')
|
|
24
|
+
|
|
25
|
+
def test_state_transitions_defined(self):
|
|
26
|
+
"""Test STATE_TRANSITIONS dict is defined."""
|
|
27
|
+
from backend.models.job import STATE_TRANSITIONS
|
|
28
|
+
assert STATE_TRANSITIONS is not None
|
|
29
|
+
assert len(STATE_TRANSITIONS) > 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestStorageServiceExtended:
|
|
33
|
+
"""Extended tests for StorageService."""
|
|
34
|
+
|
|
35
|
+
def test_storage_service_initialization(self):
|
|
36
|
+
"""Test StorageService can be initialized."""
|
|
37
|
+
with patch('backend.services.storage_service.storage'):
|
|
38
|
+
from backend.services.storage_service import StorageService
|
|
39
|
+
service = StorageService()
|
|
40
|
+
assert service is not None
|
|
41
|
+
|
|
42
|
+
def test_storage_service_bucket_name(self):
|
|
43
|
+
"""Test StorageService uses configured bucket."""
|
|
44
|
+
mock_client = MagicMock()
|
|
45
|
+
mock_bucket = MagicMock()
|
|
46
|
+
mock_client.bucket.return_value = mock_bucket
|
|
47
|
+
|
|
48
|
+
with patch('backend.services.storage_service.storage.Client', return_value=mock_client), \
|
|
49
|
+
patch.dict('os.environ', {'GCS_BUCKET_NAME': 'test-bucket'}):
|
|
50
|
+
from backend.services.storage_service import StorageService
|
|
51
|
+
service = StorageService()
|
|
52
|
+
# Service should use the configured bucket
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestAuthServiceExtended:
|
|
56
|
+
"""Extended tests for AuthService.
|
|
57
|
+
|
|
58
|
+
Note: Full auth service testing is in test_services.py.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def test_auth_service_module_imports(self):
|
|
62
|
+
"""Test AuthService module can be imported."""
|
|
63
|
+
from backend.services import auth_service
|
|
64
|
+
assert hasattr(auth_service, 'AuthService')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestWorkerServiceExtended:
|
|
68
|
+
"""Extended tests for WorkerService."""
|
|
69
|
+
|
|
70
|
+
def test_worker_service_initialization(self):
|
|
71
|
+
"""Test WorkerService can be initialized."""
|
|
72
|
+
from backend.services.worker_service import WorkerService
|
|
73
|
+
service = WorkerService()
|
|
74
|
+
assert service is not None
|
|
75
|
+
|
|
76
|
+
def test_worker_service_get_base_url(self):
|
|
77
|
+
"""Test WorkerService constructs correct base URL."""
|
|
78
|
+
from backend.services.worker_service import WorkerService
|
|
79
|
+
service = WorkerService()
|
|
80
|
+
|
|
81
|
+
url = service._get_base_url()
|
|
82
|
+
assert 'http' in url
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_worker_service_trigger_with_mock(self):
|
|
86
|
+
"""Test triggering a worker with mocked HTTP."""
|
|
87
|
+
from backend.services.worker_service import WorkerService
|
|
88
|
+
|
|
89
|
+
service = WorkerService()
|
|
90
|
+
|
|
91
|
+
# Mock the HTTP client
|
|
92
|
+
with patch('backend.services.worker_service.httpx.AsyncClient') as mock_client_cls:
|
|
93
|
+
mock_client = MagicMock()
|
|
94
|
+
mock_response = MagicMock()
|
|
95
|
+
mock_response.status_code = 200
|
|
96
|
+
mock_client.post = AsyncMock(return_value=mock_response)
|
|
97
|
+
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
|
|
98
|
+
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=None)
|
|
99
|
+
|
|
100
|
+
# The trigger methods should work with mocked HTTP
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestFirestoreServiceExtended:
|
|
104
|
+
"""Extended tests for FirestoreService."""
|
|
105
|
+
|
|
106
|
+
def test_firestore_service_initialization(self):
|
|
107
|
+
"""Test FirestoreService can be initialized."""
|
|
108
|
+
with patch('backend.services.firestore_service.firestore'):
|
|
109
|
+
from backend.services.firestore_service import FirestoreService
|
|
110
|
+
service = FirestoreService()
|
|
111
|
+
assert service is not None
|
|
112
|
+
|