karaoke-gen 0.90.1__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/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
- 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.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for dropbox_service.py - Dropbox file operations.
|
|
3
|
+
|
|
4
|
+
These tests mock the Dropbox SDK and Secret Manager to verify:
|
|
5
|
+
- Credential loading from Secret Manager
|
|
6
|
+
- Folder listing and brand code calculation
|
|
7
|
+
- File and folder uploads
|
|
8
|
+
- Shared link creation
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import pytest
|
|
13
|
+
from unittest.mock import Mock, MagicMock, patch
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestDropboxServiceInit:
|
|
17
|
+
"""Test DropboxService initialization."""
|
|
18
|
+
|
|
19
|
+
def test_init_creates_service(self):
|
|
20
|
+
"""Test initialization creates service with no client."""
|
|
21
|
+
from backend.services.dropbox_service import DropboxService
|
|
22
|
+
|
|
23
|
+
service = DropboxService()
|
|
24
|
+
|
|
25
|
+
assert service._client is None
|
|
26
|
+
assert service._is_configured is False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestLoadCredentials:
|
|
30
|
+
"""Test _load_credentials method."""
|
|
31
|
+
|
|
32
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
33
|
+
def test_load_credentials_success(self, mock_sm_client_class):
|
|
34
|
+
"""Test successful credential loading from Secret Manager."""
|
|
35
|
+
from backend.services.dropbox_service import DropboxService
|
|
36
|
+
|
|
37
|
+
mock_sm_client = Mock()
|
|
38
|
+
mock_response = Mock()
|
|
39
|
+
mock_response.payload.data = json.dumps({
|
|
40
|
+
"access_token": "access-token-123",
|
|
41
|
+
"refresh_token": "refresh-token-456",
|
|
42
|
+
"app_key": "app-key",
|
|
43
|
+
"app_secret": "app-secret",
|
|
44
|
+
}).encode("UTF-8")
|
|
45
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
46
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
47
|
+
|
|
48
|
+
service = DropboxService()
|
|
49
|
+
creds = service._load_credentials()
|
|
50
|
+
|
|
51
|
+
assert creds is not None
|
|
52
|
+
assert creds["access_token"] == "access-token-123"
|
|
53
|
+
|
|
54
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
55
|
+
def test_load_credentials_failure(self, mock_sm_client_class):
|
|
56
|
+
"""Test handling when Secret Manager fails."""
|
|
57
|
+
from backend.services.dropbox_service import DropboxService
|
|
58
|
+
|
|
59
|
+
mock_sm_client = Mock()
|
|
60
|
+
mock_sm_client.access_secret_version.side_effect = Exception("Access denied")
|
|
61
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
62
|
+
|
|
63
|
+
service = DropboxService()
|
|
64
|
+
creds = service._load_credentials()
|
|
65
|
+
|
|
66
|
+
assert creds is None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestIsConfigured:
|
|
70
|
+
"""Test is_configured property."""
|
|
71
|
+
|
|
72
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
73
|
+
def test_is_configured_true(self, mock_sm_client_class):
|
|
74
|
+
"""Test is_configured returns True when credentials available."""
|
|
75
|
+
from backend.services.dropbox_service import DropboxService
|
|
76
|
+
|
|
77
|
+
mock_sm_client = Mock()
|
|
78
|
+
mock_response = Mock()
|
|
79
|
+
mock_response.payload.data = json.dumps({
|
|
80
|
+
"access_token": "token"
|
|
81
|
+
}).encode("UTF-8")
|
|
82
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
83
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
84
|
+
|
|
85
|
+
service = DropboxService()
|
|
86
|
+
|
|
87
|
+
assert service.is_configured is True
|
|
88
|
+
|
|
89
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
90
|
+
def test_is_configured_false_no_token(self, mock_sm_client_class):
|
|
91
|
+
"""Test is_configured returns False when no access_token."""
|
|
92
|
+
from backend.services.dropbox_service import DropboxService
|
|
93
|
+
|
|
94
|
+
mock_sm_client = Mock()
|
|
95
|
+
mock_response = Mock()
|
|
96
|
+
mock_response.payload.data = json.dumps({
|
|
97
|
+
"refresh_token": "refresh" # Missing access_token
|
|
98
|
+
}).encode("UTF-8")
|
|
99
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
100
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
101
|
+
|
|
102
|
+
service = DropboxService()
|
|
103
|
+
|
|
104
|
+
assert service.is_configured is False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestDropboxClient:
|
|
108
|
+
"""Test client property."""
|
|
109
|
+
|
|
110
|
+
@patch("dropbox.Dropbox")
|
|
111
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
112
|
+
def test_client_creates_dropbox_instance(self, mock_sm_client_class, mock_dropbox_class):
|
|
113
|
+
"""Test client property creates Dropbox SDK client."""
|
|
114
|
+
from backend.services.dropbox_service import DropboxService
|
|
115
|
+
|
|
116
|
+
mock_sm_client = Mock()
|
|
117
|
+
mock_response = Mock()
|
|
118
|
+
mock_response.payload.data = json.dumps({
|
|
119
|
+
"access_token": "token",
|
|
120
|
+
"refresh_token": "refresh",
|
|
121
|
+
"app_key": "key",
|
|
122
|
+
"app_secret": "secret",
|
|
123
|
+
}).encode("UTF-8")
|
|
124
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
125
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
126
|
+
|
|
127
|
+
mock_dropbox = Mock()
|
|
128
|
+
mock_dropbox_class.return_value = mock_dropbox
|
|
129
|
+
|
|
130
|
+
service = DropboxService()
|
|
131
|
+
client = service.client
|
|
132
|
+
|
|
133
|
+
mock_dropbox_class.assert_called_once_with(
|
|
134
|
+
oauth2_access_token="token",
|
|
135
|
+
oauth2_refresh_token="refresh",
|
|
136
|
+
app_key="key",
|
|
137
|
+
app_secret="secret",
|
|
138
|
+
)
|
|
139
|
+
assert client == mock_dropbox
|
|
140
|
+
|
|
141
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
142
|
+
def test_client_raises_on_missing_credentials(self, mock_sm_client_class):
|
|
143
|
+
"""Test client raises RuntimeError when credentials missing."""
|
|
144
|
+
from backend.services.dropbox_service import DropboxService
|
|
145
|
+
|
|
146
|
+
mock_sm_client = Mock()
|
|
147
|
+
mock_sm_client.access_secret_version.side_effect = Exception("Not found")
|
|
148
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
149
|
+
|
|
150
|
+
service = DropboxService()
|
|
151
|
+
|
|
152
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
153
|
+
_ = service.client
|
|
154
|
+
|
|
155
|
+
assert "not configured" in str(exc_info.value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestListFolders:
|
|
159
|
+
"""Test list_folders method."""
|
|
160
|
+
|
|
161
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
162
|
+
def test_list_folders(self, mock_sm_client_class):
|
|
163
|
+
"""Test listing folders at a path."""
|
|
164
|
+
from backend.services.dropbox_service import DropboxService
|
|
165
|
+
from dropbox.files import FolderMetadata
|
|
166
|
+
|
|
167
|
+
mock_sm_client = Mock()
|
|
168
|
+
mock_response = Mock()
|
|
169
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
170
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
171
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
172
|
+
|
|
173
|
+
# Create mock folder entries
|
|
174
|
+
mock_folder1 = Mock(spec=FolderMetadata)
|
|
175
|
+
mock_folder1.name = "NOMAD-0001"
|
|
176
|
+
mock_folder2 = Mock(spec=FolderMetadata)
|
|
177
|
+
mock_folder2.name = "NOMAD-0002"
|
|
178
|
+
mock_file = Mock() # Not a FolderMetadata
|
|
179
|
+
|
|
180
|
+
mock_result = Mock()
|
|
181
|
+
mock_result.entries = [mock_folder1, mock_folder2, mock_file]
|
|
182
|
+
mock_result.has_more = False
|
|
183
|
+
|
|
184
|
+
service = DropboxService()
|
|
185
|
+
# Directly set the client to avoid needing to mock the whole init chain
|
|
186
|
+
mock_dropbox = Mock()
|
|
187
|
+
mock_dropbox.files_list_folder.return_value = mock_result
|
|
188
|
+
service._client = mock_dropbox
|
|
189
|
+
|
|
190
|
+
folders = service.list_folders("/Karaoke/Tracks")
|
|
191
|
+
|
|
192
|
+
mock_dropbox.files_list_folder.assert_called_once_with("/Karaoke/Tracks")
|
|
193
|
+
assert folders == ["NOMAD-0001", "NOMAD-0002"]
|
|
194
|
+
|
|
195
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
196
|
+
def test_list_folders_adds_leading_slash(self, mock_sm_client_class):
|
|
197
|
+
"""Test that path without leading slash gets one added."""
|
|
198
|
+
from backend.services.dropbox_service import DropboxService
|
|
199
|
+
|
|
200
|
+
mock_sm_client = Mock()
|
|
201
|
+
mock_response = Mock()
|
|
202
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
203
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
204
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
205
|
+
|
|
206
|
+
mock_result = Mock()
|
|
207
|
+
mock_result.entries = []
|
|
208
|
+
mock_result.has_more = False
|
|
209
|
+
|
|
210
|
+
service = DropboxService()
|
|
211
|
+
mock_dropbox = Mock()
|
|
212
|
+
mock_dropbox.files_list_folder.return_value = mock_result
|
|
213
|
+
service._client = mock_dropbox
|
|
214
|
+
|
|
215
|
+
service.list_folders("path/without/slash")
|
|
216
|
+
|
|
217
|
+
mock_dropbox.files_list_folder.assert_called_once_with("/path/without/slash")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestGetNextBrandCode:
|
|
221
|
+
"""Test get_next_brand_code method."""
|
|
222
|
+
|
|
223
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
224
|
+
def test_get_next_brand_code(self, mock_sm_client_class):
|
|
225
|
+
"""Test calculating next brand code from existing folders."""
|
|
226
|
+
from backend.services.dropbox_service import DropboxService
|
|
227
|
+
|
|
228
|
+
mock_sm_client = Mock()
|
|
229
|
+
mock_response = Mock()
|
|
230
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
231
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
232
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
233
|
+
|
|
234
|
+
service = DropboxService()
|
|
235
|
+
|
|
236
|
+
# Mock list_folders to return existing codes
|
|
237
|
+
with patch.object(service, "list_folders") as mock_list:
|
|
238
|
+
mock_list.return_value = [
|
|
239
|
+
"NOMAD-1161",
|
|
240
|
+
"NOMAD-1162",
|
|
241
|
+
"NOMAD-1163",
|
|
242
|
+
"Other Folder",
|
|
243
|
+
"NOMAD-0001",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
next_code = service.get_next_brand_code("/path", "NOMAD")
|
|
247
|
+
|
|
248
|
+
assert next_code == "NOMAD-1164"
|
|
249
|
+
|
|
250
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
251
|
+
def test_get_next_brand_code_empty_folder(self, mock_sm_client_class):
|
|
252
|
+
"""Test brand code calculation with no existing codes."""
|
|
253
|
+
from backend.services.dropbox_service import DropboxService
|
|
254
|
+
|
|
255
|
+
mock_sm_client = Mock()
|
|
256
|
+
mock_response = Mock()
|
|
257
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
258
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
259
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
260
|
+
|
|
261
|
+
service = DropboxService()
|
|
262
|
+
|
|
263
|
+
with patch.object(service, "list_folders") as mock_list:
|
|
264
|
+
mock_list.return_value = []
|
|
265
|
+
|
|
266
|
+
next_code = service.get_next_brand_code("/path", "BRAND")
|
|
267
|
+
|
|
268
|
+
assert next_code == "BRAND-0001"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestUploadFile:
|
|
272
|
+
"""Test upload_file method."""
|
|
273
|
+
|
|
274
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
275
|
+
def test_upload_small_file(self, mock_sm_client_class, tmp_path):
|
|
276
|
+
"""Test uploading a small file directly."""
|
|
277
|
+
from backend.services.dropbox_service import DropboxService
|
|
278
|
+
|
|
279
|
+
mock_sm_client = Mock()
|
|
280
|
+
mock_response = Mock()
|
|
281
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
282
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
283
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
284
|
+
|
|
285
|
+
# Create test file
|
|
286
|
+
test_file = tmp_path / "test.txt"
|
|
287
|
+
test_file.write_text("Small file content")
|
|
288
|
+
|
|
289
|
+
service = DropboxService()
|
|
290
|
+
mock_dropbox = Mock()
|
|
291
|
+
service._client = mock_dropbox
|
|
292
|
+
|
|
293
|
+
service.upload_file(str(test_file), "/Uploads/test.txt")
|
|
294
|
+
|
|
295
|
+
mock_dropbox.files_upload.assert_called_once()
|
|
296
|
+
|
|
297
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
298
|
+
def test_upload_file_adds_leading_slash(self, mock_sm_client_class, tmp_path):
|
|
299
|
+
"""Test upload adds leading slash to remote path."""
|
|
300
|
+
from backend.services.dropbox_service import DropboxService
|
|
301
|
+
|
|
302
|
+
mock_sm_client = Mock()
|
|
303
|
+
mock_response = Mock()
|
|
304
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
305
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
306
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
307
|
+
|
|
308
|
+
test_file = tmp_path / "test.txt"
|
|
309
|
+
test_file.write_text("content")
|
|
310
|
+
|
|
311
|
+
service = DropboxService()
|
|
312
|
+
mock_dropbox = Mock()
|
|
313
|
+
service._client = mock_dropbox
|
|
314
|
+
|
|
315
|
+
service.upload_file(str(test_file), "uploads/test.txt")
|
|
316
|
+
|
|
317
|
+
# Check that the path has leading slash
|
|
318
|
+
call_args = mock_dropbox.files_upload.call_args
|
|
319
|
+
assert call_args[0][1] == "/uploads/test.txt"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class TestUploadFolder:
|
|
323
|
+
"""Test upload_folder method."""
|
|
324
|
+
|
|
325
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
326
|
+
def test_upload_folder(self, mock_sm_client_class, tmp_path):
|
|
327
|
+
"""Test uploading a folder with multiple files."""
|
|
328
|
+
from backend.services.dropbox_service import DropboxService
|
|
329
|
+
|
|
330
|
+
mock_sm_client = Mock()
|
|
331
|
+
mock_response = Mock()
|
|
332
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
333
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
334
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
335
|
+
|
|
336
|
+
# Create test folder with files (no subdirs for this test)
|
|
337
|
+
(tmp_path / "file1.txt").write_text("content1")
|
|
338
|
+
(tmp_path / "file2.txt").write_text("content2")
|
|
339
|
+
|
|
340
|
+
service = DropboxService()
|
|
341
|
+
|
|
342
|
+
with patch.object(service, "upload_file") as mock_upload:
|
|
343
|
+
service.upload_folder(str(tmp_path), "/Uploads/folder")
|
|
344
|
+
|
|
345
|
+
# Should upload 2 files
|
|
346
|
+
assert mock_upload.call_count == 2
|
|
347
|
+
|
|
348
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
349
|
+
def test_upload_folder_recursive(self, mock_sm_client_class, tmp_path):
|
|
350
|
+
"""Test uploading a folder recursively includes subdirectories."""
|
|
351
|
+
from backend.services.dropbox_service import DropboxService
|
|
352
|
+
|
|
353
|
+
mock_sm_client = Mock()
|
|
354
|
+
mock_response = Mock()
|
|
355
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
356
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
357
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
358
|
+
|
|
359
|
+
# Create test folder with files and subdirectories
|
|
360
|
+
(tmp_path / "root_file.txt").write_text("root content")
|
|
361
|
+
(tmp_path / "stems").mkdir()
|
|
362
|
+
(tmp_path / "stems" / "vocals.flac").write_text("vocals")
|
|
363
|
+
(tmp_path / "stems" / "instrumental.flac").write_text("instrumental")
|
|
364
|
+
(tmp_path / "lyrics").mkdir()
|
|
365
|
+
(tmp_path / "lyrics" / "song.lrc").write_text("lyrics")
|
|
366
|
+
|
|
367
|
+
service = DropboxService()
|
|
368
|
+
|
|
369
|
+
uploaded_files = []
|
|
370
|
+
def capture_upload(local_path, remote_path):
|
|
371
|
+
uploaded_files.append(remote_path)
|
|
372
|
+
|
|
373
|
+
with patch.object(service, "upload_file", side_effect=capture_upload) as mock_upload:
|
|
374
|
+
service.upload_folder(str(tmp_path), "/Uploads/folder")
|
|
375
|
+
|
|
376
|
+
# Should upload 4 files (1 root + 2 stems + 1 lyrics)
|
|
377
|
+
assert mock_upload.call_count == 4
|
|
378
|
+
|
|
379
|
+
# Check that subdirectory structure is preserved
|
|
380
|
+
assert "/Uploads/folder/root_file.txt" in uploaded_files
|
|
381
|
+
assert "/Uploads/folder/stems/vocals.flac" in uploaded_files
|
|
382
|
+
assert "/Uploads/folder/stems/instrumental.flac" in uploaded_files
|
|
383
|
+
assert "/Uploads/folder/lyrics/song.lrc" in uploaded_files
|
|
384
|
+
|
|
385
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
386
|
+
def test_upload_folder_deeply_nested(self, mock_sm_client_class, tmp_path):
|
|
387
|
+
"""Test uploading deeply nested folder structure."""
|
|
388
|
+
from backend.services.dropbox_service import DropboxService
|
|
389
|
+
|
|
390
|
+
mock_sm_client = Mock()
|
|
391
|
+
mock_response = Mock()
|
|
392
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
393
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
394
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
395
|
+
|
|
396
|
+
# Create deeply nested structure
|
|
397
|
+
(tmp_path / "level1").mkdir()
|
|
398
|
+
(tmp_path / "level1" / "level2").mkdir()
|
|
399
|
+
(tmp_path / "level1" / "level2" / "deep_file.txt").write_text("deep")
|
|
400
|
+
|
|
401
|
+
service = DropboxService()
|
|
402
|
+
|
|
403
|
+
uploaded_files = []
|
|
404
|
+
def capture_upload(local_path, remote_path):
|
|
405
|
+
uploaded_files.append(remote_path)
|
|
406
|
+
|
|
407
|
+
with patch.object(service, "upload_file", side_effect=capture_upload):
|
|
408
|
+
service.upload_folder(str(tmp_path), "/Uploads/folder")
|
|
409
|
+
|
|
410
|
+
# Check deeply nested file is uploaded with correct path
|
|
411
|
+
assert "/Uploads/folder/level1/level2/deep_file.txt" in uploaded_files
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class TestCreateSharedLink:
|
|
415
|
+
"""Test create_shared_link method."""
|
|
416
|
+
|
|
417
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
418
|
+
def test_create_shared_link_new(self, mock_sm_client_class):
|
|
419
|
+
"""Test creating a new shared link."""
|
|
420
|
+
from backend.services.dropbox_service import DropboxService
|
|
421
|
+
|
|
422
|
+
mock_sm_client = Mock()
|
|
423
|
+
mock_response = Mock()
|
|
424
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
425
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
426
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
427
|
+
|
|
428
|
+
mock_link = Mock()
|
|
429
|
+
mock_link.url = "https://dropbox.com/s/abc123/file.mp4"
|
|
430
|
+
|
|
431
|
+
service = DropboxService()
|
|
432
|
+
mock_dropbox = Mock()
|
|
433
|
+
mock_dropbox.sharing_create_shared_link_with_settings.return_value = mock_link
|
|
434
|
+
service._client = mock_dropbox
|
|
435
|
+
|
|
436
|
+
url = service.create_shared_link("/Uploads/file.mp4")
|
|
437
|
+
|
|
438
|
+
assert url == "https://dropbox.com/s/abc123/file.mp4"
|
|
439
|
+
|
|
440
|
+
@patch("backend.services.dropbox_service.secretmanager.SecretManagerServiceClient")
|
|
441
|
+
def test_create_shared_link_existing(self, mock_sm_client_class):
|
|
442
|
+
"""Test getting existing shared link when one already exists."""
|
|
443
|
+
from backend.services.dropbox_service import DropboxService
|
|
444
|
+
from dropbox.exceptions import ApiError
|
|
445
|
+
|
|
446
|
+
mock_sm_client = Mock()
|
|
447
|
+
mock_response = Mock()
|
|
448
|
+
mock_response.payload.data = json.dumps({"access_token": "token"}).encode()
|
|
449
|
+
mock_sm_client.access_secret_version.return_value = mock_response
|
|
450
|
+
mock_sm_client_class.return_value = mock_sm_client
|
|
451
|
+
|
|
452
|
+
# Simulate "link already exists" error
|
|
453
|
+
mock_error = Mock()
|
|
454
|
+
mock_error.is_shared_link_already_exists.return_value = True
|
|
455
|
+
|
|
456
|
+
mock_existing_link = Mock()
|
|
457
|
+
mock_existing_link.url = "https://dropbox.com/s/existing/file.mp4"
|
|
458
|
+
|
|
459
|
+
mock_links_result = Mock()
|
|
460
|
+
mock_links_result.links = [mock_existing_link]
|
|
461
|
+
|
|
462
|
+
service = DropboxService()
|
|
463
|
+
mock_dropbox = Mock()
|
|
464
|
+
mock_dropbox.sharing_create_shared_link_with_settings.side_effect = \
|
|
465
|
+
ApiError("req_id", mock_error, "message", "headers")
|
|
466
|
+
mock_dropbox.sharing_list_shared_links.return_value = mock_links_result
|
|
467
|
+
service._client = mock_dropbox
|
|
468
|
+
|
|
469
|
+
url = service.create_shared_link("/Uploads/file.mp4")
|
|
470
|
+
|
|
471
|
+
assert url == "https://dropbox.com/s/existing/file.mp4"
|
|
472
|
+
|