karaoke-gen 0.86.7__py3-none-any.whl → 0.96.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/.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 +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -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 +405 -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 +842 -0
- backend/services/job_notification_service.py +271 -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/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -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 +88 -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 +339 -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 +273 -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_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/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 +525 -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/style_loader.py +3 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +187 -42
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- 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.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.86.7.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for gdrive_service.py - Google Drive file operations.
|
|
3
|
+
|
|
4
|
+
These tests mock the Google API client and Secret Manager to verify:
|
|
5
|
+
- Credential loading from Secret Manager
|
|
6
|
+
- Folder creation and lookup
|
|
7
|
+
- File uploads with proper MIME types
|
|
8
|
+
- Public share folder structure uploads
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import pytest
|
|
13
|
+
from unittest.mock import Mock, MagicMock, patch
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestGoogleDriveServiceInit:
|
|
17
|
+
"""Test GoogleDriveService initialization."""
|
|
18
|
+
|
|
19
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
20
|
+
def test_init_creates_service(self, mock_get_settings):
|
|
21
|
+
"""Test initialization creates service with settings."""
|
|
22
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
23
|
+
|
|
24
|
+
mock_settings = Mock()
|
|
25
|
+
mock_get_settings.return_value = mock_settings
|
|
26
|
+
|
|
27
|
+
service = GoogleDriveService()
|
|
28
|
+
|
|
29
|
+
assert service.settings == mock_settings
|
|
30
|
+
assert service._service is None
|
|
31
|
+
assert service._credentials_data is None
|
|
32
|
+
assert service._loaded is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestLoadCredentials:
|
|
36
|
+
"""Test _load_credentials method."""
|
|
37
|
+
|
|
38
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
39
|
+
def test_load_credentials_success(self, mock_get_settings):
|
|
40
|
+
"""Test successful credential loading from Secret Manager."""
|
|
41
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
42
|
+
|
|
43
|
+
mock_settings = Mock()
|
|
44
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
45
|
+
"token": "access-token",
|
|
46
|
+
"refresh_token": "refresh-token",
|
|
47
|
+
"client_id": "client-id",
|
|
48
|
+
"client_secret": "client-secret",
|
|
49
|
+
})
|
|
50
|
+
mock_get_settings.return_value = mock_settings
|
|
51
|
+
|
|
52
|
+
service = GoogleDriveService()
|
|
53
|
+
creds = service._load_credentials()
|
|
54
|
+
|
|
55
|
+
assert creds is not None
|
|
56
|
+
assert creds["refresh_token"] == "refresh-token"
|
|
57
|
+
mock_settings.get_secret.assert_called_once_with("gdrive-oauth-credentials")
|
|
58
|
+
|
|
59
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
60
|
+
def test_load_credentials_fallback_to_youtube(self, mock_get_settings):
|
|
61
|
+
"""Test fallback to YouTube credentials when Drive creds not found."""
|
|
62
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
63
|
+
|
|
64
|
+
mock_settings = Mock()
|
|
65
|
+
# First call returns None (Drive creds not found)
|
|
66
|
+
# Second call returns YouTube creds
|
|
67
|
+
mock_settings.get_secret.side_effect = [
|
|
68
|
+
None,
|
|
69
|
+
json.dumps({
|
|
70
|
+
"refresh_token": "youtube-token",
|
|
71
|
+
"client_id": "youtube-id",
|
|
72
|
+
"client_secret": "youtube-secret",
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
mock_get_settings.return_value = mock_settings
|
|
76
|
+
|
|
77
|
+
service = GoogleDriveService()
|
|
78
|
+
creds = service._load_credentials()
|
|
79
|
+
|
|
80
|
+
assert creds is not None
|
|
81
|
+
assert creds["refresh_token"] == "youtube-token"
|
|
82
|
+
assert mock_settings.get_secret.call_count == 2
|
|
83
|
+
|
|
84
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
85
|
+
def test_load_credentials_not_found(self, mock_get_settings):
|
|
86
|
+
"""Test handling when no credentials found."""
|
|
87
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
88
|
+
|
|
89
|
+
mock_settings = Mock()
|
|
90
|
+
mock_settings.get_secret.return_value = None
|
|
91
|
+
mock_get_settings.return_value = mock_settings
|
|
92
|
+
|
|
93
|
+
service = GoogleDriveService()
|
|
94
|
+
creds = service._load_credentials()
|
|
95
|
+
|
|
96
|
+
assert creds is None
|
|
97
|
+
assert service._loaded is True
|
|
98
|
+
|
|
99
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
100
|
+
def test_load_credentials_missing_required_fields(self, mock_get_settings):
|
|
101
|
+
"""Test handling when credentials missing required fields."""
|
|
102
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
103
|
+
|
|
104
|
+
mock_settings = Mock()
|
|
105
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
106
|
+
"token": "access-token",
|
|
107
|
+
# Missing: refresh_token, client_id, client_secret
|
|
108
|
+
})
|
|
109
|
+
mock_get_settings.return_value = mock_settings
|
|
110
|
+
|
|
111
|
+
service = GoogleDriveService()
|
|
112
|
+
creds = service._load_credentials()
|
|
113
|
+
|
|
114
|
+
assert creds is None
|
|
115
|
+
|
|
116
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
117
|
+
def test_load_credentials_cached(self, mock_get_settings):
|
|
118
|
+
"""Test credentials are cached after first load."""
|
|
119
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
120
|
+
|
|
121
|
+
mock_settings = Mock()
|
|
122
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
123
|
+
"refresh_token": "token",
|
|
124
|
+
"client_id": "id",
|
|
125
|
+
"client_secret": "secret",
|
|
126
|
+
})
|
|
127
|
+
mock_get_settings.return_value = mock_settings
|
|
128
|
+
|
|
129
|
+
service = GoogleDriveService()
|
|
130
|
+
|
|
131
|
+
creds1 = service._load_credentials()
|
|
132
|
+
creds2 = service._load_credentials()
|
|
133
|
+
|
|
134
|
+
assert creds1 == creds2
|
|
135
|
+
assert mock_settings.get_secret.call_count == 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestIsConfigured:
|
|
139
|
+
"""Test is_configured property."""
|
|
140
|
+
|
|
141
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
142
|
+
def test_is_configured_true(self, mock_get_settings):
|
|
143
|
+
"""Test is_configured returns True when credentials exist."""
|
|
144
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
145
|
+
|
|
146
|
+
mock_settings = Mock()
|
|
147
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
148
|
+
"refresh_token": "token",
|
|
149
|
+
"client_id": "id",
|
|
150
|
+
"client_secret": "secret",
|
|
151
|
+
})
|
|
152
|
+
mock_get_settings.return_value = mock_settings
|
|
153
|
+
|
|
154
|
+
service = GoogleDriveService()
|
|
155
|
+
|
|
156
|
+
assert service.is_configured is True
|
|
157
|
+
|
|
158
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
159
|
+
def test_is_configured_false(self, mock_get_settings):
|
|
160
|
+
"""Test is_configured returns False when no credentials."""
|
|
161
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
162
|
+
|
|
163
|
+
mock_settings = Mock()
|
|
164
|
+
mock_settings.get_secret.return_value = None
|
|
165
|
+
mock_get_settings.return_value = mock_settings
|
|
166
|
+
|
|
167
|
+
service = GoogleDriveService()
|
|
168
|
+
|
|
169
|
+
assert service.is_configured is False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestDriveService:
|
|
173
|
+
"""Test service property."""
|
|
174
|
+
|
|
175
|
+
@patch("googleapiclient.discovery.build")
|
|
176
|
+
@patch("google.oauth2.credentials.Credentials")
|
|
177
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
178
|
+
def test_service_creates_drive_client(
|
|
179
|
+
self, mock_get_settings, mock_creds_class, mock_build
|
|
180
|
+
):
|
|
181
|
+
"""Test service property creates Google Drive API client."""
|
|
182
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
183
|
+
|
|
184
|
+
mock_settings = Mock()
|
|
185
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
186
|
+
"token": "access-token",
|
|
187
|
+
"refresh_token": "refresh-token",
|
|
188
|
+
"client_id": "client-id",
|
|
189
|
+
"client_secret": "client-secret",
|
|
190
|
+
})
|
|
191
|
+
mock_get_settings.return_value = mock_settings
|
|
192
|
+
|
|
193
|
+
mock_creds = Mock()
|
|
194
|
+
mock_creds_class.return_value = mock_creds
|
|
195
|
+
|
|
196
|
+
mock_drive_service = Mock()
|
|
197
|
+
mock_build.return_value = mock_drive_service
|
|
198
|
+
|
|
199
|
+
service = GoogleDriveService()
|
|
200
|
+
result = service.service
|
|
201
|
+
|
|
202
|
+
mock_build.assert_called_once_with("drive", "v3", credentials=mock_creds)
|
|
203
|
+
assert result == mock_drive_service
|
|
204
|
+
|
|
205
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
206
|
+
def test_service_raises_on_missing_credentials(self, mock_get_settings):
|
|
207
|
+
"""Test service raises RuntimeError when credentials missing."""
|
|
208
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
209
|
+
|
|
210
|
+
mock_settings = Mock()
|
|
211
|
+
mock_settings.get_secret.return_value = None
|
|
212
|
+
mock_get_settings.return_value = mock_settings
|
|
213
|
+
|
|
214
|
+
service = GoogleDriveService()
|
|
215
|
+
|
|
216
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
217
|
+
_ = service.service
|
|
218
|
+
|
|
219
|
+
assert "not configured" in str(exc_info.value)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestGetOrCreateFolder:
|
|
223
|
+
"""Test get_or_create_folder method."""
|
|
224
|
+
|
|
225
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
226
|
+
def test_get_existing_folder(self, mock_get_settings):
|
|
227
|
+
"""Test finding an existing folder."""
|
|
228
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
229
|
+
|
|
230
|
+
mock_settings = Mock()
|
|
231
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
232
|
+
"refresh_token": "token",
|
|
233
|
+
"client_id": "id",
|
|
234
|
+
"client_secret": "secret",
|
|
235
|
+
})
|
|
236
|
+
mock_get_settings.return_value = mock_settings
|
|
237
|
+
|
|
238
|
+
service = GoogleDriveService()
|
|
239
|
+
|
|
240
|
+
# Mock the Drive API
|
|
241
|
+
mock_files_api = Mock()
|
|
242
|
+
mock_list_result = Mock()
|
|
243
|
+
mock_list_result.execute.return_value = {
|
|
244
|
+
"files": [{"id": "folder-id-123", "name": "MP4"}]
|
|
245
|
+
}
|
|
246
|
+
mock_files_api.list.return_value = mock_list_result
|
|
247
|
+
|
|
248
|
+
mock_drive = Mock()
|
|
249
|
+
mock_drive.files.return_value = mock_files_api
|
|
250
|
+
service._service = mock_drive
|
|
251
|
+
service._loaded = True
|
|
252
|
+
service._credentials_data = {"refresh_token": "token", "client_id": "id", "client_secret": "secret"}
|
|
253
|
+
|
|
254
|
+
folder_id = service.get_or_create_folder("parent-123", "MP4")
|
|
255
|
+
|
|
256
|
+
assert folder_id == "folder-id-123"
|
|
257
|
+
|
|
258
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
259
|
+
def test_create_new_folder(self, mock_get_settings):
|
|
260
|
+
"""Test creating a new folder when it doesn't exist."""
|
|
261
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
262
|
+
|
|
263
|
+
mock_settings = Mock()
|
|
264
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
265
|
+
"refresh_token": "token",
|
|
266
|
+
"client_id": "id",
|
|
267
|
+
"client_secret": "secret",
|
|
268
|
+
})
|
|
269
|
+
mock_get_settings.return_value = mock_settings
|
|
270
|
+
|
|
271
|
+
service = GoogleDriveService()
|
|
272
|
+
|
|
273
|
+
# Mock the Drive API
|
|
274
|
+
mock_files_api = Mock()
|
|
275
|
+
|
|
276
|
+
# list returns empty (folder doesn't exist)
|
|
277
|
+
mock_list_result = Mock()
|
|
278
|
+
mock_list_result.execute.return_value = {"files": []}
|
|
279
|
+
mock_files_api.list.return_value = mock_list_result
|
|
280
|
+
|
|
281
|
+
# create returns new folder
|
|
282
|
+
mock_create_result = Mock()
|
|
283
|
+
mock_create_result.execute.return_value = {"id": "new-folder-id"}
|
|
284
|
+
mock_files_api.create.return_value = mock_create_result
|
|
285
|
+
|
|
286
|
+
mock_drive = Mock()
|
|
287
|
+
mock_drive.files.return_value = mock_files_api
|
|
288
|
+
service._service = mock_drive
|
|
289
|
+
service._loaded = True
|
|
290
|
+
service._credentials_data = {"refresh_token": "token", "client_id": "id", "client_secret": "secret"}
|
|
291
|
+
|
|
292
|
+
folder_id = service.get_or_create_folder("parent-123", "NewFolder")
|
|
293
|
+
|
|
294
|
+
assert folder_id == "new-folder-id"
|
|
295
|
+
mock_files_api.create.assert_called_once()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TestUploadFile:
|
|
299
|
+
"""Test upload_file method."""
|
|
300
|
+
|
|
301
|
+
@patch("googleapiclient.http.MediaFileUpload")
|
|
302
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
303
|
+
def test_upload_file_success(self, mock_get_settings, mock_media, tmp_path):
|
|
304
|
+
"""Test uploading a file to Drive."""
|
|
305
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
306
|
+
|
|
307
|
+
mock_settings = Mock()
|
|
308
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
309
|
+
"refresh_token": "token",
|
|
310
|
+
"client_id": "id",
|
|
311
|
+
"client_secret": "secret",
|
|
312
|
+
})
|
|
313
|
+
mock_get_settings.return_value = mock_settings
|
|
314
|
+
|
|
315
|
+
# Create test file
|
|
316
|
+
test_file = tmp_path / "test.mp4"
|
|
317
|
+
test_file.write_bytes(b"video content")
|
|
318
|
+
|
|
319
|
+
service = GoogleDriveService()
|
|
320
|
+
|
|
321
|
+
mock_files_api = Mock()
|
|
322
|
+
|
|
323
|
+
# list returns empty (no existing file)
|
|
324
|
+
mock_list_result = Mock()
|
|
325
|
+
mock_list_result.execute.return_value = {"files": []}
|
|
326
|
+
mock_files_api.list.return_value = mock_list_result
|
|
327
|
+
|
|
328
|
+
# create returns new file
|
|
329
|
+
mock_create_result = Mock()
|
|
330
|
+
mock_create_result.execute.return_value = {"id": "file-id-123"}
|
|
331
|
+
mock_files_api.create.return_value = mock_create_result
|
|
332
|
+
|
|
333
|
+
mock_drive = Mock()
|
|
334
|
+
mock_drive.files.return_value = mock_files_api
|
|
335
|
+
service._service = mock_drive
|
|
336
|
+
service._loaded = True
|
|
337
|
+
service._credentials_data = {"refresh_token": "token", "client_id": "id", "client_secret": "secret"}
|
|
338
|
+
|
|
339
|
+
file_id = service.upload_file(str(test_file), "parent-123", "video.mp4")
|
|
340
|
+
|
|
341
|
+
assert file_id == "file-id-123"
|
|
342
|
+
mock_media.assert_called_once()
|
|
343
|
+
# Verify MIME type was set correctly
|
|
344
|
+
call_kwargs = mock_media.call_args.kwargs
|
|
345
|
+
assert call_kwargs["mimetype"] == "video/mp4"
|
|
346
|
+
|
|
347
|
+
@patch("googleapiclient.http.MediaFileUpload")
|
|
348
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
349
|
+
def test_upload_file_replaces_existing(self, mock_get_settings, mock_media, tmp_path):
|
|
350
|
+
"""Test upload deletes existing file before upload."""
|
|
351
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
352
|
+
|
|
353
|
+
mock_settings = Mock()
|
|
354
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
355
|
+
"refresh_token": "token",
|
|
356
|
+
"client_id": "id",
|
|
357
|
+
"client_secret": "secret",
|
|
358
|
+
})
|
|
359
|
+
mock_get_settings.return_value = mock_settings
|
|
360
|
+
|
|
361
|
+
test_file = tmp_path / "test.mp4"
|
|
362
|
+
test_file.write_bytes(b"content")
|
|
363
|
+
|
|
364
|
+
service = GoogleDriveService()
|
|
365
|
+
|
|
366
|
+
mock_files_api = Mock()
|
|
367
|
+
|
|
368
|
+
# list returns existing file
|
|
369
|
+
mock_list_result = Mock()
|
|
370
|
+
mock_list_result.execute.return_value = {
|
|
371
|
+
"files": [{"id": "existing-file-id"}]
|
|
372
|
+
}
|
|
373
|
+
mock_files_api.list.return_value = mock_list_result
|
|
374
|
+
|
|
375
|
+
mock_delete_result = Mock()
|
|
376
|
+
mock_delete_result.execute.return_value = {}
|
|
377
|
+
mock_files_api.delete.return_value = mock_delete_result
|
|
378
|
+
|
|
379
|
+
mock_create_result = Mock()
|
|
380
|
+
mock_create_result.execute.return_value = {"id": "new-file-id"}
|
|
381
|
+
mock_files_api.create.return_value = mock_create_result
|
|
382
|
+
|
|
383
|
+
mock_drive = Mock()
|
|
384
|
+
mock_drive.files.return_value = mock_files_api
|
|
385
|
+
service._service = mock_drive
|
|
386
|
+
service._loaded = True
|
|
387
|
+
service._credentials_data = {"refresh_token": "token", "client_id": "id", "client_secret": "secret"}
|
|
388
|
+
|
|
389
|
+
file_id = service.upload_file(str(test_file), "parent-123", "video.mp4")
|
|
390
|
+
|
|
391
|
+
# Should have deleted existing file
|
|
392
|
+
mock_files_api.delete.assert_called_once_with(fileId="existing-file-id")
|
|
393
|
+
assert file_id == "new-file-id"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class TestUploadToPublicShare:
|
|
397
|
+
"""Test upload_to_public_share method."""
|
|
398
|
+
|
|
399
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
400
|
+
def test_upload_to_public_share(self, mock_get_settings, tmp_path):
|
|
401
|
+
"""Test uploading files to public share folder structure."""
|
|
402
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
403
|
+
|
|
404
|
+
mock_settings = Mock()
|
|
405
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
406
|
+
"refresh_token": "token",
|
|
407
|
+
"client_id": "id",
|
|
408
|
+
"client_secret": "secret",
|
|
409
|
+
})
|
|
410
|
+
mock_get_settings.return_value = mock_settings
|
|
411
|
+
|
|
412
|
+
# Create test files
|
|
413
|
+
mp4_file = tmp_path / "output.mp4"
|
|
414
|
+
mp4_file.write_bytes(b"4k video")
|
|
415
|
+
mp4_720_file = tmp_path / "output_720p.mp4"
|
|
416
|
+
mp4_720_file.write_bytes(b"720p video")
|
|
417
|
+
cdg_file = tmp_path / "output.zip"
|
|
418
|
+
cdg_file.write_bytes(b"cdg package")
|
|
419
|
+
|
|
420
|
+
service = GoogleDriveService()
|
|
421
|
+
|
|
422
|
+
# Mock methods
|
|
423
|
+
with patch.object(service, "get_or_create_folder") as mock_get_folder:
|
|
424
|
+
with patch.object(service, "upload_file") as mock_upload:
|
|
425
|
+
mock_get_folder.side_effect = [
|
|
426
|
+
"mp4-folder-id",
|
|
427
|
+
"mp4-720-folder-id",
|
|
428
|
+
"cdg-folder-id",
|
|
429
|
+
]
|
|
430
|
+
mock_upload.side_effect = [
|
|
431
|
+
"mp4-file-id",
|
|
432
|
+
"720p-file-id",
|
|
433
|
+
"cdg-file-id",
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
output_files = {
|
|
437
|
+
"final_karaoke_lossy_mp4": str(mp4_file),
|
|
438
|
+
"final_karaoke_lossy_720p_mp4": str(mp4_720_file),
|
|
439
|
+
"final_karaoke_cdg_zip": str(cdg_file),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
result = service.upload_to_public_share(
|
|
443
|
+
root_folder_id="root-123",
|
|
444
|
+
brand_code="NOMAD-1163",
|
|
445
|
+
base_name="Artist - Title",
|
|
446
|
+
output_files=output_files,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Should have created/found 3 folders
|
|
450
|
+
assert mock_get_folder.call_count == 3
|
|
451
|
+
|
|
452
|
+
# Should have uploaded 3 files
|
|
453
|
+
assert mock_upload.call_count == 3
|
|
454
|
+
|
|
455
|
+
# Check result
|
|
456
|
+
assert result["mp4"] == "mp4-file-id"
|
|
457
|
+
assert result["mp4_720p"] == "720p-file-id"
|
|
458
|
+
assert result["cdg"] == "cdg-file-id"
|
|
459
|
+
|
|
460
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
461
|
+
def test_upload_to_public_share_skips_missing_files(
|
|
462
|
+
self, mock_get_settings, tmp_path
|
|
463
|
+
):
|
|
464
|
+
"""Test upload skips files that don't exist."""
|
|
465
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
466
|
+
|
|
467
|
+
mock_settings = Mock()
|
|
468
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
469
|
+
"refresh_token": "token",
|
|
470
|
+
"client_id": "id",
|
|
471
|
+
"client_secret": "secret",
|
|
472
|
+
})
|
|
473
|
+
mock_get_settings.return_value = mock_settings
|
|
474
|
+
|
|
475
|
+
# Only create one file
|
|
476
|
+
mp4_file = tmp_path / "output.mp4"
|
|
477
|
+
mp4_file.write_bytes(b"video")
|
|
478
|
+
|
|
479
|
+
service = GoogleDriveService()
|
|
480
|
+
|
|
481
|
+
with patch.object(service, "get_or_create_folder") as mock_get_folder:
|
|
482
|
+
with patch.object(service, "upload_file") as mock_upload:
|
|
483
|
+
mock_get_folder.return_value = "folder-id"
|
|
484
|
+
mock_upload.return_value = "file-id"
|
|
485
|
+
|
|
486
|
+
output_files = {
|
|
487
|
+
"final_karaoke_lossy_mp4": str(mp4_file),
|
|
488
|
+
"final_karaoke_lossy_720p_mp4": "/nonexistent/file.mp4",
|
|
489
|
+
"final_karaoke_cdg_zip": None,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
result = service.upload_to_public_share(
|
|
493
|
+
root_folder_id="root-123",
|
|
494
|
+
brand_code="CODE",
|
|
495
|
+
base_name="Name",
|
|
496
|
+
output_files=output_files,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Should only upload the one file that exists
|
|
500
|
+
assert mock_upload.call_count == 1
|
|
501
|
+
assert len(result) == 1
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class TestGetGdriveService:
|
|
505
|
+
"""Test get_gdrive_service singleton."""
|
|
506
|
+
|
|
507
|
+
@patch("backend.services.gdrive_service.get_settings")
|
|
508
|
+
def test_get_gdrive_service_singleton(self, mock_get_settings):
|
|
509
|
+
"""Test get_gdrive_service returns singleton instance."""
|
|
510
|
+
from backend.services.gdrive_service import get_gdrive_service
|
|
511
|
+
import backend.services.gdrive_service as gdrive_module
|
|
512
|
+
|
|
513
|
+
# Reset singleton
|
|
514
|
+
gdrive_module._gdrive_service = None
|
|
515
|
+
|
|
516
|
+
mock_settings = Mock()
|
|
517
|
+
mock_settings.get_secret.return_value = None
|
|
518
|
+
mock_get_settings.return_value = mock_settings
|
|
519
|
+
|
|
520
|
+
service1 = get_gdrive_service()
|
|
521
|
+
service2 = get_gdrive_service()
|
|
522
|
+
|
|
523
|
+
assert service1 is service2
|
|
524
|
+
|