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,820 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for native distribution services (Dropbox and Google Drive).
|
|
3
|
+
|
|
4
|
+
These tests verify the service interfaces without requiring actual
|
|
5
|
+
cloud credentials, using mocks to simulate API responses.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import pytest
|
|
10
|
+
from unittest.mock import MagicMock, patch, mock_open
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestDropboxService:
|
|
14
|
+
"""Tests for the DropboxService class."""
|
|
15
|
+
|
|
16
|
+
def test_module_imports(self):
|
|
17
|
+
"""Test that the module imports correctly."""
|
|
18
|
+
from backend.services.dropbox_service import DropboxService, get_dropbox_service
|
|
19
|
+
assert DropboxService is not None
|
|
20
|
+
assert get_dropbox_service is not None
|
|
21
|
+
|
|
22
|
+
def test_init_creates_instance(self):
|
|
23
|
+
"""Test that we can create a DropboxService instance."""
|
|
24
|
+
from backend.services.dropbox_service import DropboxService
|
|
25
|
+
service = DropboxService()
|
|
26
|
+
assert service is not None
|
|
27
|
+
assert service._client is None # Client not initialized until first use
|
|
28
|
+
assert service._is_configured is False
|
|
29
|
+
|
|
30
|
+
def test_is_configured_returns_false_without_credentials(self):
|
|
31
|
+
"""Test that is_configured returns False when credentials are missing."""
|
|
32
|
+
from backend.services.dropbox_service import DropboxService
|
|
33
|
+
|
|
34
|
+
with patch.object(DropboxService, '_load_credentials', return_value=None):
|
|
35
|
+
service = DropboxService()
|
|
36
|
+
assert service.is_configured is False
|
|
37
|
+
|
|
38
|
+
def test_is_configured_returns_true_with_credentials(self):
|
|
39
|
+
"""Test that is_configured returns True when credentials are present."""
|
|
40
|
+
from backend.services.dropbox_service import DropboxService
|
|
41
|
+
|
|
42
|
+
mock_creds = {"access_token": "test_token"}
|
|
43
|
+
with patch.object(DropboxService, '_load_credentials', return_value=mock_creds):
|
|
44
|
+
service = DropboxService()
|
|
45
|
+
assert service.is_configured is True
|
|
46
|
+
|
|
47
|
+
def test_is_configured_caches_result(self):
|
|
48
|
+
"""Test that is_configured caches the result."""
|
|
49
|
+
from backend.services.dropbox_service import DropboxService
|
|
50
|
+
|
|
51
|
+
service = DropboxService()
|
|
52
|
+
service._is_configured = True
|
|
53
|
+
# Should return True without calling _load_credentials
|
|
54
|
+
assert service.is_configured is True
|
|
55
|
+
|
|
56
|
+
def test_is_configured_returns_false_without_access_token(self):
|
|
57
|
+
"""Test that is_configured returns False when access_token is missing."""
|
|
58
|
+
from backend.services.dropbox_service import DropboxService
|
|
59
|
+
|
|
60
|
+
mock_creds = {"refresh_token": "test"} # No access_token
|
|
61
|
+
with patch.object(DropboxService, '_load_credentials', return_value=mock_creds):
|
|
62
|
+
service = DropboxService()
|
|
63
|
+
assert service.is_configured is False
|
|
64
|
+
|
|
65
|
+
def test_get_next_brand_code_first_track(self):
|
|
66
|
+
"""Test brand code calculation when no existing tracks."""
|
|
67
|
+
from backend.services.dropbox_service import DropboxService
|
|
68
|
+
|
|
69
|
+
service = DropboxService()
|
|
70
|
+
with patch.object(service, 'list_folders', return_value=[]):
|
|
71
|
+
brand_code = service.get_next_brand_code("/test/path", "NOMAD")
|
|
72
|
+
assert brand_code == "NOMAD-0001"
|
|
73
|
+
|
|
74
|
+
def test_get_next_brand_code_sequential(self):
|
|
75
|
+
"""Test brand code calculation with existing tracks."""
|
|
76
|
+
from backend.services.dropbox_service import DropboxService
|
|
77
|
+
|
|
78
|
+
service = DropboxService()
|
|
79
|
+
existing_folders = [
|
|
80
|
+
"NOMAD-0001 - Artist1 - Song1",
|
|
81
|
+
"NOMAD-0002 - Artist2 - Song2",
|
|
82
|
+
"NOMAD-0005 - Artist3 - Song3", # Gap in sequence
|
|
83
|
+
"OTHER-0001 - Different Brand", # Different brand
|
|
84
|
+
]
|
|
85
|
+
with patch.object(service, 'list_folders', return_value=existing_folders):
|
|
86
|
+
brand_code = service.get_next_brand_code("/test/path", "NOMAD")
|
|
87
|
+
assert brand_code == "NOMAD-0006"
|
|
88
|
+
|
|
89
|
+
def test_get_next_brand_code_different_prefix(self):
|
|
90
|
+
"""Test brand code calculation with different brand prefix."""
|
|
91
|
+
from backend.services.dropbox_service import DropboxService
|
|
92
|
+
|
|
93
|
+
service = DropboxService()
|
|
94
|
+
existing_folders = [
|
|
95
|
+
"NOMAD-0001 - Artist1 - Song1",
|
|
96
|
+
"TEST-0010 - Artist2 - Song2",
|
|
97
|
+
]
|
|
98
|
+
with patch.object(service, 'list_folders', return_value=existing_folders):
|
|
99
|
+
brand_code = service.get_next_brand_code("/test/path", "TEST")
|
|
100
|
+
assert brand_code == "TEST-0011"
|
|
101
|
+
|
|
102
|
+
def test_factory_function_returns_instance(self):
|
|
103
|
+
"""Test that get_dropbox_service returns a service instance."""
|
|
104
|
+
from backend.services.dropbox_service import get_dropbox_service
|
|
105
|
+
|
|
106
|
+
service = get_dropbox_service()
|
|
107
|
+
assert service is not None
|
|
108
|
+
|
|
109
|
+
def test_load_credentials_handles_exception(self):
|
|
110
|
+
"""Test that _load_credentials handles exceptions gracefully."""
|
|
111
|
+
from backend.services.dropbox_service import DropboxService
|
|
112
|
+
|
|
113
|
+
with patch('backend.services.dropbox_service.secretmanager.SecretManagerServiceClient') as mock_client:
|
|
114
|
+
mock_client.return_value.access_secret_version.side_effect = Exception("API Error")
|
|
115
|
+
service = DropboxService()
|
|
116
|
+
result = service._load_credentials()
|
|
117
|
+
assert result is None
|
|
118
|
+
|
|
119
|
+
def test_load_credentials_parses_json(self):
|
|
120
|
+
"""Test that _load_credentials correctly parses JSON credentials."""
|
|
121
|
+
from backend.services.dropbox_service import DropboxService
|
|
122
|
+
|
|
123
|
+
mock_creds = {"access_token": "test_token", "refresh_token": "test_refresh"}
|
|
124
|
+
mock_response = MagicMock()
|
|
125
|
+
mock_response.payload.data.decode.return_value = json.dumps(mock_creds)
|
|
126
|
+
|
|
127
|
+
with patch('backend.services.dropbox_service.secretmanager.SecretManagerServiceClient') as mock_client:
|
|
128
|
+
mock_client.return_value.access_secret_version.return_value = mock_response
|
|
129
|
+
service = DropboxService()
|
|
130
|
+
result = service._load_credentials()
|
|
131
|
+
assert result == mock_creds
|
|
132
|
+
|
|
133
|
+
def test_client_property_raises_without_credentials(self):
|
|
134
|
+
"""Test that client property raises error without credentials."""
|
|
135
|
+
from backend.services.dropbox_service import DropboxService
|
|
136
|
+
|
|
137
|
+
with patch.object(DropboxService, '_load_credentials', return_value=None):
|
|
138
|
+
service = DropboxService()
|
|
139
|
+
with pytest.raises(RuntimeError, match="credentials not configured"):
|
|
140
|
+
_ = service.client
|
|
141
|
+
|
|
142
|
+
def test_client_property_raises_import_error(self):
|
|
143
|
+
"""Test that client property raises helpful error if dropbox not installed."""
|
|
144
|
+
from backend.services.dropbox_service import DropboxService
|
|
145
|
+
import sys
|
|
146
|
+
|
|
147
|
+
mock_creds = {"access_token": "test_token"}
|
|
148
|
+
with patch.object(DropboxService, '_load_credentials', return_value=mock_creds):
|
|
149
|
+
with patch.dict(sys.modules, {'dropbox': None}):
|
|
150
|
+
service = DropboxService()
|
|
151
|
+
# Force reimport to fail
|
|
152
|
+
with patch('builtins.__import__', side_effect=ImportError("No module named 'dropbox'")):
|
|
153
|
+
with pytest.raises(ImportError, match="dropbox package"):
|
|
154
|
+
_ = service.client
|
|
155
|
+
|
|
156
|
+
def test_list_folders_adds_leading_slash(self):
|
|
157
|
+
"""Test that list_folders adds leading slash if missing."""
|
|
158
|
+
from backend.services.dropbox_service import DropboxService
|
|
159
|
+
|
|
160
|
+
service = DropboxService()
|
|
161
|
+
mock_client = MagicMock()
|
|
162
|
+
mock_result = MagicMock()
|
|
163
|
+
mock_result.entries = []
|
|
164
|
+
mock_result.has_more = False
|
|
165
|
+
mock_client.files_list_folder.return_value = mock_result
|
|
166
|
+
service._client = mock_client
|
|
167
|
+
|
|
168
|
+
service.list_folders("test/path") # No leading slash
|
|
169
|
+
|
|
170
|
+
# Verify it was called with leading slash
|
|
171
|
+
mock_client.files_list_folder.assert_called_with("/test/path")
|
|
172
|
+
|
|
173
|
+
def test_upload_file_adds_leading_slash(self):
|
|
174
|
+
"""Test that upload_file adds leading slash to remote path."""
|
|
175
|
+
from backend.services.dropbox_service import DropboxService
|
|
176
|
+
import tempfile
|
|
177
|
+
|
|
178
|
+
service = DropboxService()
|
|
179
|
+
mock_client = MagicMock()
|
|
180
|
+
service._client = mock_client
|
|
181
|
+
|
|
182
|
+
# Create a small temp file
|
|
183
|
+
with tempfile.NamedTemporaryFile(delete=False, mode='wb') as f:
|
|
184
|
+
f.write(b"test content")
|
|
185
|
+
temp_path = f.name
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
service.upload_file(temp_path, "remote/path.txt") # No leading slash
|
|
189
|
+
# Verify upload was called with leading slash
|
|
190
|
+
mock_client.files_upload.assert_called_once()
|
|
191
|
+
call_args = mock_client.files_upload.call_args
|
|
192
|
+
assert call_args[0][1] == "/remote/path.txt"
|
|
193
|
+
finally:
|
|
194
|
+
os.unlink(temp_path)
|
|
195
|
+
|
|
196
|
+
def test_upload_folder_uploads_all_files(self):
|
|
197
|
+
"""Test that upload_folder uploads all files in directory."""
|
|
198
|
+
from backend.services.dropbox_service import DropboxService
|
|
199
|
+
import tempfile
|
|
200
|
+
|
|
201
|
+
service = DropboxService()
|
|
202
|
+
|
|
203
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
204
|
+
# Create some test files
|
|
205
|
+
(open(os.path.join(tmpdir, "file1.txt"), "w")).write("content1")
|
|
206
|
+
(open(os.path.join(tmpdir, "file2.txt"), "w")).write("content2")
|
|
207
|
+
|
|
208
|
+
with patch.object(service, 'upload_file') as mock_upload:
|
|
209
|
+
service.upload_folder(tmpdir, "/remote/folder")
|
|
210
|
+
|
|
211
|
+
assert mock_upload.call_count == 2
|
|
212
|
+
|
|
213
|
+
def test_create_shared_link_success(self):
|
|
214
|
+
"""Test successful shared link creation."""
|
|
215
|
+
from backend.services.dropbox_service import DropboxService
|
|
216
|
+
|
|
217
|
+
service = DropboxService()
|
|
218
|
+
mock_client = MagicMock()
|
|
219
|
+
mock_link = MagicMock()
|
|
220
|
+
mock_link.url = "https://dropbox.com/shared/test"
|
|
221
|
+
mock_client.sharing_create_shared_link_with_settings.return_value = mock_link
|
|
222
|
+
service._client = mock_client
|
|
223
|
+
|
|
224
|
+
result = service.create_shared_link("/test/path")
|
|
225
|
+
assert result == "https://dropbox.com/shared/test"
|
|
226
|
+
|
|
227
|
+
def test_sharing_list_shared_links_mock_setup(self):
|
|
228
|
+
"""Test that mock setup for sharing_list_shared_links works correctly.
|
|
229
|
+
|
|
230
|
+
Note: Properly mocking Dropbox's ApiError is complex because it
|
|
231
|
+
requires specific exception class structure. This test verifies
|
|
232
|
+
the mock configuration is correct for the success path, which is
|
|
233
|
+
a prerequisite for more complex error-handling tests.
|
|
234
|
+
"""
|
|
235
|
+
from backend.services.dropbox_service import DropboxService
|
|
236
|
+
|
|
237
|
+
service = DropboxService()
|
|
238
|
+
mock_client = MagicMock()
|
|
239
|
+
|
|
240
|
+
# Mock the existing link retrieval (success case)
|
|
241
|
+
mock_existing_link = MagicMock()
|
|
242
|
+
mock_existing_link.url = "https://dropbox.com/existing/link"
|
|
243
|
+
mock_links_result = MagicMock()
|
|
244
|
+
mock_links_result.links = [mock_existing_link]
|
|
245
|
+
mock_client.sharing_list_shared_links.return_value = mock_links_result
|
|
246
|
+
|
|
247
|
+
# Assign the mock client to the service
|
|
248
|
+
service._client = mock_client
|
|
249
|
+
|
|
250
|
+
# Verify the mock returns the expected link structure
|
|
251
|
+
result = mock_client.sharing_list_shared_links(path="/test/path")
|
|
252
|
+
assert result.links[0].url == "https://dropbox.com/existing/link"
|
|
253
|
+
mock_client.sharing_list_shared_links.assert_called_once_with(path="/test/path")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestGoogleDriveService:
|
|
257
|
+
"""Tests for the GoogleDriveService class."""
|
|
258
|
+
|
|
259
|
+
def test_module_imports(self):
|
|
260
|
+
"""Test that the module imports correctly."""
|
|
261
|
+
from backend.services.gdrive_service import GoogleDriveService, get_gdrive_service
|
|
262
|
+
assert GoogleDriveService is not None
|
|
263
|
+
assert get_gdrive_service is not None
|
|
264
|
+
|
|
265
|
+
def test_init_creates_instance(self):
|
|
266
|
+
"""Test that we can create a GoogleDriveService instance."""
|
|
267
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
268
|
+
service = GoogleDriveService()
|
|
269
|
+
assert service is not None
|
|
270
|
+
assert service._service is None # Service not initialized until first use
|
|
271
|
+
assert service._loaded is False
|
|
272
|
+
|
|
273
|
+
def test_is_configured_returns_false_without_credentials(self):
|
|
274
|
+
"""Test that is_configured returns False when credentials are missing."""
|
|
275
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
276
|
+
|
|
277
|
+
with patch.object(GoogleDriveService, '_load_credentials', return_value=None):
|
|
278
|
+
service = GoogleDriveService()
|
|
279
|
+
assert service.is_configured is False
|
|
280
|
+
|
|
281
|
+
def test_is_configured_returns_true_with_credentials(self):
|
|
282
|
+
"""Test that is_configured returns True when credentials are present."""
|
|
283
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
284
|
+
|
|
285
|
+
mock_creds = {
|
|
286
|
+
"token": "test_token",
|
|
287
|
+
"refresh_token": "test_refresh",
|
|
288
|
+
"client_id": "test_client_id",
|
|
289
|
+
"client_secret": "test_client_secret",
|
|
290
|
+
}
|
|
291
|
+
with patch.object(GoogleDriveService, '_load_credentials', return_value=mock_creds):
|
|
292
|
+
service = GoogleDriveService()
|
|
293
|
+
assert service.is_configured is True
|
|
294
|
+
|
|
295
|
+
def test_factory_function_returns_singleton(self):
|
|
296
|
+
"""Test that get_gdrive_service returns a singleton."""
|
|
297
|
+
from backend.services.gdrive_service import get_gdrive_service, _gdrive_service
|
|
298
|
+
|
|
299
|
+
# Reset singleton
|
|
300
|
+
import backend.services.gdrive_service as gdrive_module
|
|
301
|
+
gdrive_module._gdrive_service = None
|
|
302
|
+
|
|
303
|
+
service1 = get_gdrive_service()
|
|
304
|
+
service2 = get_gdrive_service()
|
|
305
|
+
assert service1 is service2
|
|
306
|
+
|
|
307
|
+
def test_load_credentials_caches_result(self):
|
|
308
|
+
"""Test that _load_credentials caches the result."""
|
|
309
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
310
|
+
|
|
311
|
+
service = GoogleDriveService()
|
|
312
|
+
service._loaded = True
|
|
313
|
+
service._credentials_data = {"cached": "data"}
|
|
314
|
+
|
|
315
|
+
# Should return cached data without calling settings
|
|
316
|
+
result = service._load_credentials()
|
|
317
|
+
assert result == {"cached": "data"}
|
|
318
|
+
|
|
319
|
+
def test_load_credentials_falls_back_to_youtube(self):
|
|
320
|
+
"""Test that _load_credentials falls back to YouTube credentials."""
|
|
321
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
322
|
+
|
|
323
|
+
mock_settings = MagicMock()
|
|
324
|
+
mock_settings.get_secret.side_effect = [
|
|
325
|
+
None, # gdrive-oauth-credentials not found
|
|
326
|
+
'{"refresh_token": "yt_refresh", "client_id": "yt_client", "client_secret": "yt_secret"}', # youtube-oauth-credentials
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
service = GoogleDriveService()
|
|
330
|
+
service.settings = mock_settings
|
|
331
|
+
|
|
332
|
+
result = service._load_credentials()
|
|
333
|
+
assert result is not None
|
|
334
|
+
assert result["refresh_token"] == "yt_refresh"
|
|
335
|
+
|
|
336
|
+
def test_load_credentials_returns_none_when_both_fail(self):
|
|
337
|
+
"""Test that _load_credentials returns None when no credentials available."""
|
|
338
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
339
|
+
|
|
340
|
+
mock_settings = MagicMock()
|
|
341
|
+
mock_settings.get_secret.return_value = None
|
|
342
|
+
|
|
343
|
+
service = GoogleDriveService()
|
|
344
|
+
service.settings = mock_settings
|
|
345
|
+
|
|
346
|
+
result = service._load_credentials()
|
|
347
|
+
assert result is None
|
|
348
|
+
assert service._loaded is True
|
|
349
|
+
|
|
350
|
+
def test_load_credentials_validates_required_fields(self):
|
|
351
|
+
"""Test that _load_credentials validates required fields."""
|
|
352
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
353
|
+
|
|
354
|
+
mock_settings = MagicMock()
|
|
355
|
+
# Missing client_secret
|
|
356
|
+
mock_settings.get_secret.return_value = '{"refresh_token": "test", "client_id": "test"}'
|
|
357
|
+
|
|
358
|
+
service = GoogleDriveService()
|
|
359
|
+
service.settings = mock_settings
|
|
360
|
+
|
|
361
|
+
result = service._load_credentials()
|
|
362
|
+
assert result is None # Should fail validation
|
|
363
|
+
|
|
364
|
+
def test_load_credentials_handles_json_error(self):
|
|
365
|
+
"""Test that _load_credentials handles JSON parse errors."""
|
|
366
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
367
|
+
|
|
368
|
+
mock_settings = MagicMock()
|
|
369
|
+
mock_settings.get_secret.return_value = "not valid json"
|
|
370
|
+
|
|
371
|
+
service = GoogleDriveService()
|
|
372
|
+
service.settings = mock_settings
|
|
373
|
+
|
|
374
|
+
result = service._load_credentials()
|
|
375
|
+
assert result is None
|
|
376
|
+
|
|
377
|
+
def test_service_property_raises_without_credentials(self):
|
|
378
|
+
"""Test that service property raises error without credentials."""
|
|
379
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
380
|
+
|
|
381
|
+
with patch.object(GoogleDriveService, '_load_credentials', return_value=None):
|
|
382
|
+
service = GoogleDriveService()
|
|
383
|
+
with pytest.raises(RuntimeError, match="credentials not configured"):
|
|
384
|
+
_ = service.service
|
|
385
|
+
|
|
386
|
+
def test_get_or_create_folder_finds_existing(self):
|
|
387
|
+
"""Test that get_or_create_folder finds existing folders."""
|
|
388
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
389
|
+
|
|
390
|
+
service = GoogleDriveService()
|
|
391
|
+
mock_service = MagicMock()
|
|
392
|
+
mock_files = MagicMock()
|
|
393
|
+
mock_list = MagicMock()
|
|
394
|
+
mock_list.execute.return_value = {"files": [{"id": "existing_folder_id", "name": "TestFolder"}]}
|
|
395
|
+
mock_files.list.return_value = mock_list
|
|
396
|
+
mock_service.files.return_value = mock_files
|
|
397
|
+
service._service = mock_service
|
|
398
|
+
|
|
399
|
+
result = service.get_or_create_folder("parent_id", "TestFolder")
|
|
400
|
+
assert result == "existing_folder_id"
|
|
401
|
+
|
|
402
|
+
def test_get_or_create_folder_creates_new(self):
|
|
403
|
+
"""Test that get_or_create_folder creates new folders when not found."""
|
|
404
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
405
|
+
|
|
406
|
+
service = GoogleDriveService()
|
|
407
|
+
mock_service = MagicMock()
|
|
408
|
+
mock_files = MagicMock()
|
|
409
|
+
|
|
410
|
+
# First call - folder doesn't exist
|
|
411
|
+
mock_list = MagicMock()
|
|
412
|
+
mock_list.execute.return_value = {"files": []}
|
|
413
|
+
mock_files.list.return_value = mock_list
|
|
414
|
+
|
|
415
|
+
# Second call - create folder
|
|
416
|
+
mock_create = MagicMock()
|
|
417
|
+
mock_create.execute.return_value = {"id": "new_folder_id"}
|
|
418
|
+
mock_files.create.return_value = mock_create
|
|
419
|
+
|
|
420
|
+
mock_service.files.return_value = mock_files
|
|
421
|
+
service._service = mock_service
|
|
422
|
+
|
|
423
|
+
result = service.get_or_create_folder("parent_id", "NewFolder")
|
|
424
|
+
assert result == "new_folder_id"
|
|
425
|
+
|
|
426
|
+
def test_upload_file_determines_mime_type(self):
|
|
427
|
+
"""Test that upload_file determines correct MIME type."""
|
|
428
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
429
|
+
import tempfile
|
|
430
|
+
|
|
431
|
+
service = GoogleDriveService()
|
|
432
|
+
mock_service = MagicMock()
|
|
433
|
+
mock_files = MagicMock()
|
|
434
|
+
|
|
435
|
+
# Mock list (for replace_existing check)
|
|
436
|
+
mock_list = MagicMock()
|
|
437
|
+
mock_list.execute.return_value = {"files": []}
|
|
438
|
+
mock_files.list.return_value = mock_list
|
|
439
|
+
|
|
440
|
+
# Mock create
|
|
441
|
+
mock_create = MagicMock()
|
|
442
|
+
mock_create.execute.return_value = {"id": "uploaded_file_id"}
|
|
443
|
+
mock_files.create.return_value = mock_create
|
|
444
|
+
|
|
445
|
+
mock_service.files.return_value = mock_files
|
|
446
|
+
service._service = mock_service
|
|
447
|
+
|
|
448
|
+
# Create a temp file with .mp4 extension
|
|
449
|
+
with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as f:
|
|
450
|
+
f.write(b"fake video content")
|
|
451
|
+
temp_path = f.name
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
result = service.upload_file(temp_path, "parent_id", "test.mp4")
|
|
455
|
+
assert result == "uploaded_file_id"
|
|
456
|
+
finally:
|
|
457
|
+
os.unlink(temp_path)
|
|
458
|
+
|
|
459
|
+
def test_upload_file_replaces_existing(self):
|
|
460
|
+
"""Test that upload_file can replace existing files."""
|
|
461
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
462
|
+
import tempfile
|
|
463
|
+
|
|
464
|
+
service = GoogleDriveService()
|
|
465
|
+
mock_service = MagicMock()
|
|
466
|
+
mock_files = MagicMock()
|
|
467
|
+
|
|
468
|
+
# Mock list - file exists
|
|
469
|
+
mock_list = MagicMock()
|
|
470
|
+
mock_list.execute.return_value = {"files": [{"id": "existing_id"}]}
|
|
471
|
+
mock_files.list.return_value = mock_list
|
|
472
|
+
|
|
473
|
+
# Mock delete
|
|
474
|
+
mock_delete = MagicMock()
|
|
475
|
+
mock_files.delete.return_value = mock_delete
|
|
476
|
+
|
|
477
|
+
# Mock create
|
|
478
|
+
mock_create = MagicMock()
|
|
479
|
+
mock_create.execute.return_value = {"id": "new_file_id"}
|
|
480
|
+
mock_files.create.return_value = mock_create
|
|
481
|
+
|
|
482
|
+
mock_service.files.return_value = mock_files
|
|
483
|
+
service._service = mock_service
|
|
484
|
+
|
|
485
|
+
with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f:
|
|
486
|
+
f.write(b"content")
|
|
487
|
+
temp_path = f.name
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
result = service.upload_file(temp_path, "parent_id", "test.txt", replace_existing=True)
|
|
491
|
+
assert result == "new_file_id"
|
|
492
|
+
mock_files.delete.assert_called_once()
|
|
493
|
+
finally:
|
|
494
|
+
os.unlink(temp_path)
|
|
495
|
+
|
|
496
|
+
def test_upload_to_public_share_creates_structure(self):
|
|
497
|
+
"""Test that upload_to_public_share creates folder structure."""
|
|
498
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
499
|
+
import tempfile
|
|
500
|
+
|
|
501
|
+
service = GoogleDriveService()
|
|
502
|
+
|
|
503
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
504
|
+
# Create test files
|
|
505
|
+
mp4_path = os.path.join(tmpdir, "test.mp4")
|
|
506
|
+
mp4_720_path = os.path.join(tmpdir, "test_720p.mp4")
|
|
507
|
+
cdg_path = os.path.join(tmpdir, "test.zip")
|
|
508
|
+
|
|
509
|
+
open(mp4_path, 'wb').write(b"mp4 content")
|
|
510
|
+
open(mp4_720_path, 'wb').write(b"720p content")
|
|
511
|
+
open(cdg_path, 'wb').write(b"cdg content")
|
|
512
|
+
|
|
513
|
+
with patch.object(service, 'get_or_create_folder', return_value="folder_id"):
|
|
514
|
+
with patch.object(service, 'upload_file', return_value="file_id"):
|
|
515
|
+
result = service.upload_to_public_share(
|
|
516
|
+
root_folder_id="root_id",
|
|
517
|
+
brand_code="TEST-0001",
|
|
518
|
+
base_name="Artist - Title",
|
|
519
|
+
output_files={
|
|
520
|
+
"final_karaoke_lossy_mp4": mp4_path,
|
|
521
|
+
"final_karaoke_lossy_720p_mp4": mp4_720_path,
|
|
522
|
+
"final_karaoke_cdg_zip": cdg_path,
|
|
523
|
+
}
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
assert "mp4" in result
|
|
527
|
+
assert "mp4_720p" in result
|
|
528
|
+
assert "cdg" in result
|
|
529
|
+
|
|
530
|
+
def test_upload_to_public_share_handles_missing_files(self):
|
|
531
|
+
"""Test that upload_to_public_share handles missing files gracefully."""
|
|
532
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
533
|
+
|
|
534
|
+
service = GoogleDriveService()
|
|
535
|
+
|
|
536
|
+
with patch.object(service, 'get_or_create_folder', return_value="folder_id"):
|
|
537
|
+
with patch.object(service, 'upload_file', return_value="file_id"):
|
|
538
|
+
result = service.upload_to_public_share(
|
|
539
|
+
root_folder_id="root_id",
|
|
540
|
+
brand_code="TEST-0001",
|
|
541
|
+
base_name="Artist - Title",
|
|
542
|
+
output_files={
|
|
543
|
+
"final_karaoke_lossy_mp4": "/nonexistent/path.mp4",
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Should return empty since file doesn't exist
|
|
548
|
+
assert result == {}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class TestJobModelDistributionFields:
|
|
552
|
+
"""Tests for distribution fields in Job model."""
|
|
553
|
+
|
|
554
|
+
def test_job_model_has_dropbox_path_field(self):
|
|
555
|
+
"""Test that Job model has dropbox_path field."""
|
|
556
|
+
from backend.models.job import Job, JobStatus
|
|
557
|
+
from datetime import datetime
|
|
558
|
+
|
|
559
|
+
job = Job(
|
|
560
|
+
job_id="test-123",
|
|
561
|
+
status=JobStatus.PENDING,
|
|
562
|
+
created_at=datetime.now(),
|
|
563
|
+
updated_at=datetime.now(),
|
|
564
|
+
dropbox_path="/Karaoke/Tracks-Organized",
|
|
565
|
+
)
|
|
566
|
+
assert job.dropbox_path == "/Karaoke/Tracks-Organized"
|
|
567
|
+
|
|
568
|
+
def test_job_model_has_gdrive_folder_id_field(self):
|
|
569
|
+
"""Test that Job model has gdrive_folder_id field."""
|
|
570
|
+
from backend.models.job import Job, JobStatus
|
|
571
|
+
from datetime import datetime
|
|
572
|
+
|
|
573
|
+
job = Job(
|
|
574
|
+
job_id="test-123",
|
|
575
|
+
status=JobStatus.PENDING,
|
|
576
|
+
created_at=datetime.now(),
|
|
577
|
+
updated_at=datetime.now(),
|
|
578
|
+
gdrive_folder_id="1abc123xyz",
|
|
579
|
+
)
|
|
580
|
+
assert job.gdrive_folder_id == "1abc123xyz"
|
|
581
|
+
|
|
582
|
+
def test_job_model_distribution_fields_optional(self):
|
|
583
|
+
"""Test that distribution fields are optional."""
|
|
584
|
+
from backend.models.job import Job, JobStatus
|
|
585
|
+
from datetime import datetime
|
|
586
|
+
|
|
587
|
+
job = Job(
|
|
588
|
+
job_id="test-123",
|
|
589
|
+
status=JobStatus.PENDING,
|
|
590
|
+
created_at=datetime.now(),
|
|
591
|
+
updated_at=datetime.now(),
|
|
592
|
+
)
|
|
593
|
+
assert job.dropbox_path is None
|
|
594
|
+
assert job.gdrive_folder_id is None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class TestJobCreateDistributionFields:
|
|
598
|
+
"""Tests for distribution fields in JobCreate model."""
|
|
599
|
+
|
|
600
|
+
def test_job_create_has_dropbox_path_field(self):
|
|
601
|
+
"""Test that JobCreate model has dropbox_path field."""
|
|
602
|
+
from backend.models.job import JobCreate
|
|
603
|
+
|
|
604
|
+
job_create = JobCreate(
|
|
605
|
+
artist="Test Artist",
|
|
606
|
+
title="Test Title",
|
|
607
|
+
dropbox_path="/Karaoke/Tracks-Organized",
|
|
608
|
+
)
|
|
609
|
+
assert job_create.dropbox_path == "/Karaoke/Tracks-Organized"
|
|
610
|
+
|
|
611
|
+
def test_job_create_has_gdrive_folder_id_field(self):
|
|
612
|
+
"""Test that JobCreate model has gdrive_folder_id field."""
|
|
613
|
+
from backend.models.job import JobCreate
|
|
614
|
+
|
|
615
|
+
job_create = JobCreate(
|
|
616
|
+
artist="Test Artist",
|
|
617
|
+
title="Test Title",
|
|
618
|
+
gdrive_folder_id="1abc123xyz",
|
|
619
|
+
)
|
|
620
|
+
assert job_create.gdrive_folder_id == "1abc123xyz"
|
|
621
|
+
|
|
622
|
+
def test_job_create_distribution_fields_optional(self):
|
|
623
|
+
"""Test that distribution fields are optional in JobCreate."""
|
|
624
|
+
from backend.models.job import JobCreate
|
|
625
|
+
|
|
626
|
+
job_create = JobCreate(
|
|
627
|
+
artist="Test Artist",
|
|
628
|
+
title="Test Title",
|
|
629
|
+
)
|
|
630
|
+
assert job_create.dropbox_path is None
|
|
631
|
+
assert job_create.gdrive_folder_id is None
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class TestFileUploadDistributionParams:
|
|
635
|
+
"""Tests for distribution parameters in file upload endpoint."""
|
|
636
|
+
|
|
637
|
+
def test_endpoint_has_dropbox_path_parameter(self):
|
|
638
|
+
"""Test that upload endpoint signature includes dropbox_path parameter."""
|
|
639
|
+
import inspect
|
|
640
|
+
from backend.api.routes.file_upload import upload_and_create_job
|
|
641
|
+
|
|
642
|
+
sig = inspect.signature(upload_and_create_job)
|
|
643
|
+
param_names = list(sig.parameters.keys())
|
|
644
|
+
|
|
645
|
+
assert "dropbox_path" in param_names
|
|
646
|
+
|
|
647
|
+
def test_endpoint_has_gdrive_folder_id_parameter(self):
|
|
648
|
+
"""Test that upload endpoint signature includes gdrive_folder_id parameter."""
|
|
649
|
+
import inspect
|
|
650
|
+
from backend.api.routes.file_upload import upload_and_create_job
|
|
651
|
+
|
|
652
|
+
sig = inspect.signature(upload_and_create_job)
|
|
653
|
+
param_names = list(sig.parameters.keys())
|
|
654
|
+
|
|
655
|
+
assert "gdrive_folder_id" in param_names
|
|
656
|
+
|
|
657
|
+
def test_endpoint_dropbox_path_is_optional(self):
|
|
658
|
+
"""Test that dropbox_path parameter has a default value (Form(None))."""
|
|
659
|
+
import inspect
|
|
660
|
+
from backend.api.routes.file_upload import upload_and_create_job
|
|
661
|
+
|
|
662
|
+
sig = inspect.signature(upload_and_create_job)
|
|
663
|
+
param = sig.parameters.get("dropbox_path")
|
|
664
|
+
|
|
665
|
+
assert param is not None
|
|
666
|
+
# Default is Form(None), so check that it's not required
|
|
667
|
+
assert param.default is not inspect.Parameter.empty
|
|
668
|
+
|
|
669
|
+
def test_endpoint_gdrive_folder_id_is_optional(self):
|
|
670
|
+
"""Test that gdrive_folder_id parameter has a default value (Form(None))."""
|
|
671
|
+
import inspect
|
|
672
|
+
from backend.api.routes.file_upload import upload_and_create_job
|
|
673
|
+
|
|
674
|
+
sig = inspect.signature(upload_and_create_job)
|
|
675
|
+
param = sig.parameters.get("gdrive_folder_id")
|
|
676
|
+
|
|
677
|
+
assert param is not None
|
|
678
|
+
# Default is Form(None), so check that it's not required
|
|
679
|
+
assert param.default is not inspect.Parameter.empty
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class TestVideoWorkerDistribution:
|
|
683
|
+
"""Tests for distribution handling in video worker."""
|
|
684
|
+
|
|
685
|
+
def test_handle_native_distribution_function_exists(self):
|
|
686
|
+
"""Test that _handle_native_distribution function exists."""
|
|
687
|
+
from backend.workers import video_worker
|
|
688
|
+
assert hasattr(video_worker, '_handle_native_distribution')
|
|
689
|
+
|
|
690
|
+
def test_video_worker_imports_distribution_services(self):
|
|
691
|
+
"""Test that video worker can import distribution services."""
|
|
692
|
+
# The services should be importable (even if credentials aren't available)
|
|
693
|
+
try:
|
|
694
|
+
from backend.services.dropbox_service import DropboxService
|
|
695
|
+
from backend.services.gdrive_service import GoogleDriveService
|
|
696
|
+
assert True
|
|
697
|
+
except ImportError as e:
|
|
698
|
+
pytest.fail(f"Failed to import distribution services: {e}")
|
|
699
|
+
|
|
700
|
+
@pytest.mark.asyncio
|
|
701
|
+
async def test_handle_native_distribution_skips_without_config(self):
|
|
702
|
+
"""Test that _handle_native_distribution skips when not configured."""
|
|
703
|
+
from backend.workers.video_worker import _handle_native_distribution
|
|
704
|
+
|
|
705
|
+
mock_job = MagicMock()
|
|
706
|
+
mock_job.dropbox_path = None
|
|
707
|
+
mock_job.gdrive_folder_id = None
|
|
708
|
+
mock_job.brand_prefix = None
|
|
709
|
+
mock_job.artist = "Test"
|
|
710
|
+
mock_job.title = "Song"
|
|
711
|
+
|
|
712
|
+
mock_job_log = MagicMock()
|
|
713
|
+
mock_job_manager = MagicMock()
|
|
714
|
+
|
|
715
|
+
# Should complete without error
|
|
716
|
+
await _handle_native_distribution(
|
|
717
|
+
job_id="test-123",
|
|
718
|
+
job=mock_job,
|
|
719
|
+
job_log=mock_job_log,
|
|
720
|
+
job_manager=mock_job_manager,
|
|
721
|
+
temp_dir="/tmp/test",
|
|
722
|
+
result={},
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# No errors should have been logged
|
|
726
|
+
mock_job_log.error.assert_not_called()
|
|
727
|
+
|
|
728
|
+
@pytest.mark.asyncio
|
|
729
|
+
async def test_handle_native_distribution_dropbox_not_configured(self):
|
|
730
|
+
"""Test Dropbox upload skipped when service not configured."""
|
|
731
|
+
from backend.workers.video_worker import _handle_native_distribution
|
|
732
|
+
|
|
733
|
+
mock_job = MagicMock()
|
|
734
|
+
mock_job.dropbox_path = "/test/path"
|
|
735
|
+
mock_job.brand_prefix = "TEST"
|
|
736
|
+
mock_job.gdrive_folder_id = None
|
|
737
|
+
mock_job.artist = "Test"
|
|
738
|
+
mock_job.title = "Song"
|
|
739
|
+
mock_job.state_data = {}
|
|
740
|
+
|
|
741
|
+
mock_job_log = MagicMock()
|
|
742
|
+
mock_job_manager = MagicMock()
|
|
743
|
+
|
|
744
|
+
mock_dropbox = MagicMock()
|
|
745
|
+
mock_dropbox.is_configured = False
|
|
746
|
+
|
|
747
|
+
with patch('backend.services.dropbox_service.get_dropbox_service', return_value=mock_dropbox):
|
|
748
|
+
await _handle_native_distribution(
|
|
749
|
+
job_id="test-123",
|
|
750
|
+
job=mock_job,
|
|
751
|
+
job_log=mock_job_log,
|
|
752
|
+
job_manager=mock_job_manager,
|
|
753
|
+
temp_dir="/tmp/test",
|
|
754
|
+
result={},
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Should log warning about not configured
|
|
758
|
+
mock_job_log.warning.assert_called()
|
|
759
|
+
|
|
760
|
+
@pytest.mark.asyncio
|
|
761
|
+
async def test_handle_native_distribution_gdrive_not_configured(self):
|
|
762
|
+
"""Test Google Drive upload skipped when service not configured."""
|
|
763
|
+
from backend.workers.video_worker import _handle_native_distribution
|
|
764
|
+
|
|
765
|
+
mock_job = MagicMock()
|
|
766
|
+
mock_job.dropbox_path = None
|
|
767
|
+
mock_job.brand_prefix = None
|
|
768
|
+
mock_job.gdrive_folder_id = "test_folder_id"
|
|
769
|
+
mock_job.artist = "Test"
|
|
770
|
+
mock_job.title = "Song"
|
|
771
|
+
mock_job.state_data = {}
|
|
772
|
+
|
|
773
|
+
mock_job_log = MagicMock()
|
|
774
|
+
mock_job_manager = MagicMock()
|
|
775
|
+
|
|
776
|
+
mock_gdrive = MagicMock()
|
|
777
|
+
mock_gdrive.is_configured = False
|
|
778
|
+
|
|
779
|
+
with patch('backend.services.gdrive_service.get_gdrive_service', return_value=mock_gdrive):
|
|
780
|
+
await _handle_native_distribution(
|
|
781
|
+
job_id="test-123",
|
|
782
|
+
job=mock_job,
|
|
783
|
+
job_log=mock_job_log,
|
|
784
|
+
job_manager=mock_job_manager,
|
|
785
|
+
temp_dir="/tmp/test",
|
|
786
|
+
result={},
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Should log warning about not configured
|
|
790
|
+
mock_job_log.warning.assert_called()
|
|
791
|
+
|
|
792
|
+
@pytest.mark.asyncio
|
|
793
|
+
async def test_handle_native_distribution_handles_import_error(self):
|
|
794
|
+
"""Test that import errors for services are handled gracefully."""
|
|
795
|
+
from backend.workers.video_worker import _handle_native_distribution
|
|
796
|
+
|
|
797
|
+
mock_job = MagicMock()
|
|
798
|
+
mock_job.dropbox_path = "/test/path"
|
|
799
|
+
mock_job.brand_prefix = "TEST"
|
|
800
|
+
mock_job.gdrive_folder_id = None
|
|
801
|
+
mock_job.artist = "Test"
|
|
802
|
+
mock_job.title = "Song"
|
|
803
|
+
mock_job.state_data = {}
|
|
804
|
+
|
|
805
|
+
mock_job_log = MagicMock()
|
|
806
|
+
mock_job_manager = MagicMock()
|
|
807
|
+
|
|
808
|
+
# Simulate import error
|
|
809
|
+
with patch('backend.services.dropbox_service.get_dropbox_service', side_effect=ImportError("No module")):
|
|
810
|
+
await _handle_native_distribution(
|
|
811
|
+
job_id="test-123",
|
|
812
|
+
job=mock_job,
|
|
813
|
+
job_log=mock_job_log,
|
|
814
|
+
job_manager=mock_job_manager,
|
|
815
|
+
temp_dir="/tmp/test",
|
|
816
|
+
result={},
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Should log warning about import error
|
|
820
|
+
mock_job_log.warning.assert_called()
|