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,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extended worker tests focusing on helper functions and utilities.
|
|
3
|
+
|
|
4
|
+
These tests increase coverage without requiring complex external mocking.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
from datetime import datetime, UTC
|
|
11
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
12
|
+
|
|
13
|
+
from backend.models.job import Job, JobStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestAudioWorkerHelpers:
|
|
17
|
+
"""Tests for audio_worker.py helper functions and utilities."""
|
|
18
|
+
|
|
19
|
+
def test_audio_worker_module_structure(self):
|
|
20
|
+
"""Test audio worker module has expected structure."""
|
|
21
|
+
from backend.workers import audio_worker
|
|
22
|
+
assert hasattr(audio_worker, 'process_audio_separation')
|
|
23
|
+
assert hasattr(audio_worker, 'download_audio')
|
|
24
|
+
assert hasattr(audio_worker, 'upload_separation_results')
|
|
25
|
+
|
|
26
|
+
def test_audio_worker_logger_configured(self):
|
|
27
|
+
"""Test audio worker has logger configured."""
|
|
28
|
+
from backend.workers.audio_worker import logger
|
|
29
|
+
assert logger is not None
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_upload_separation_results_with_empty_result(self):
|
|
33
|
+
"""Test upload with empty separation result."""
|
|
34
|
+
from backend.workers.audio_worker import upload_separation_results
|
|
35
|
+
|
|
36
|
+
mock_storage = MagicMock()
|
|
37
|
+
mock_job_manager = MagicMock()
|
|
38
|
+
mock_job_manager.get_job.return_value = Job(
|
|
39
|
+
job_id="test",
|
|
40
|
+
status=JobStatus.SEPARATING_STAGE2,
|
|
41
|
+
created_at=datetime.now(UTC),
|
|
42
|
+
updated_at=datetime.now(UTC)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Empty result should not crash
|
|
46
|
+
await upload_separation_results(
|
|
47
|
+
"test123",
|
|
48
|
+
{"clean_instrumental": {}, "other_stems": {}, "backing_vocals": {}, "combined_instrumentals": {}},
|
|
49
|
+
mock_storage,
|
|
50
|
+
mock_job_manager
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestLyricsWorkerHelpers:
|
|
55
|
+
"""Tests for lyrics_worker.py helper functions."""
|
|
56
|
+
|
|
57
|
+
def test_lyrics_worker_module_structure(self):
|
|
58
|
+
"""Test lyrics worker module has expected structure."""
|
|
59
|
+
from backend.workers import lyrics_worker
|
|
60
|
+
assert hasattr(lyrics_worker, 'process_lyrics_transcription')
|
|
61
|
+
assert hasattr(lyrics_worker, 'upload_lyrics_results')
|
|
62
|
+
|
|
63
|
+
def test_lyrics_worker_logger_configured(self):
|
|
64
|
+
"""Test lyrics worker has logger configured."""
|
|
65
|
+
from backend.workers.lyrics_worker import logger
|
|
66
|
+
assert logger is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestScreensWorkerHelpers:
|
|
70
|
+
"""Tests for screens_worker.py helper functions."""
|
|
71
|
+
|
|
72
|
+
def test_screens_worker_module_structure(self):
|
|
73
|
+
"""Test screens worker module has expected structure."""
|
|
74
|
+
from backend.workers import screens_worker
|
|
75
|
+
assert hasattr(screens_worker, 'generate_screens')
|
|
76
|
+
|
|
77
|
+
def test_screens_worker_logger_configured(self):
|
|
78
|
+
"""Test screens worker has logger configured."""
|
|
79
|
+
from backend.workers.screens_worker import logger
|
|
80
|
+
assert logger is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestVideoWorkerHelpers:
|
|
84
|
+
"""Tests for video_worker.py helper functions."""
|
|
85
|
+
|
|
86
|
+
def test_video_worker_module_structure(self):
|
|
87
|
+
"""Test video worker module has expected structure."""
|
|
88
|
+
from backend.workers import video_worker
|
|
89
|
+
assert hasattr(video_worker, 'generate_video')
|
|
90
|
+
|
|
91
|
+
def test_video_worker_logger_configured(self):
|
|
92
|
+
"""Test video worker has logger configured."""
|
|
93
|
+
from backend.workers.video_worker import logger
|
|
94
|
+
assert logger is not None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestWorkerJobValidation:
|
|
98
|
+
"""Tests for job validation in workers."""
|
|
99
|
+
|
|
100
|
+
@pytest.fixture
|
|
101
|
+
def mock_storage(self):
|
|
102
|
+
return MagicMock()
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def mock_job_manager(self):
|
|
106
|
+
manager = MagicMock()
|
|
107
|
+
manager.mark_job_failed = MagicMock()
|
|
108
|
+
return manager
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_audio_worker_handles_missing_job(self, mock_storage, mock_job_manager):
|
|
112
|
+
"""Test audio worker handles missing job gracefully."""
|
|
113
|
+
mock_job_manager.get_job.return_value = None
|
|
114
|
+
|
|
115
|
+
with patch('backend.workers.audio_worker.JobManager', return_value=mock_job_manager), \
|
|
116
|
+
patch('backend.workers.audio_worker.StorageService', return_value=mock_storage):
|
|
117
|
+
from backend.workers.audio_worker import process_audio_separation
|
|
118
|
+
|
|
119
|
+
await process_audio_separation("nonexistent")
|
|
120
|
+
# Should handle gracefully, potentially marking as failed
|
|
121
|
+
|
|
122
|
+
@pytest.mark.asyncio
|
|
123
|
+
async def test_lyrics_worker_handles_missing_job(self, mock_storage, mock_job_manager):
|
|
124
|
+
"""Test lyrics worker handles missing job gracefully."""
|
|
125
|
+
mock_job_manager.get_job.return_value = None
|
|
126
|
+
|
|
127
|
+
with patch('backend.workers.lyrics_worker.JobManager', return_value=mock_job_manager), \
|
|
128
|
+
patch('backend.workers.lyrics_worker.StorageService', return_value=mock_storage):
|
|
129
|
+
from backend.workers.lyrics_worker import process_lyrics_transcription
|
|
130
|
+
|
|
131
|
+
await process_lyrics_transcription("nonexistent")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestWorkerFileOperations:
|
|
135
|
+
"""Tests for worker file operations."""
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_download_audio_from_gcs_path(self):
|
|
139
|
+
"""Test downloading audio from GCS path."""
|
|
140
|
+
from backend.workers.audio_worker import download_audio
|
|
141
|
+
|
|
142
|
+
mock_storage = MagicMock()
|
|
143
|
+
mock_storage.download_file.return_value = "/tmp/test.flac"
|
|
144
|
+
|
|
145
|
+
mock_job = Job(
|
|
146
|
+
job_id="test123",
|
|
147
|
+
status=JobStatus.DOWNLOADING,
|
|
148
|
+
created_at=datetime.now(UTC),
|
|
149
|
+
updated_at=datetime.now(UTC),
|
|
150
|
+
input_media_gcs_path="uploads/test123/song.flac"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
154
|
+
result = await download_audio("test123", temp_dir, mock_storage, mock_job)
|
|
155
|
+
mock_storage.download_file.assert_called()
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_lyrics_download_audio(self):
|
|
159
|
+
"""Test lyrics worker downloads audio."""
|
|
160
|
+
from backend.workers.lyrics_worker import download_audio
|
|
161
|
+
|
|
162
|
+
mock_storage = MagicMock()
|
|
163
|
+
mock_storage.download_file.return_value = "/tmp/test.flac"
|
|
164
|
+
|
|
165
|
+
mock_job_manager = MagicMock()
|
|
166
|
+
mock_job = Job(
|
|
167
|
+
job_id="test123",
|
|
168
|
+
status=JobStatus.TRANSCRIBING,
|
|
169
|
+
created_at=datetime.now(UTC),
|
|
170
|
+
updated_at=datetime.now(UTC),
|
|
171
|
+
input_media_gcs_path="uploads/test123/song.flac"
|
|
172
|
+
)
|
|
173
|
+
mock_job_manager.get_job.return_value = mock_job
|
|
174
|
+
|
|
175
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
176
|
+
result = await download_audio("test123", temp_dir, mock_storage, mock_job, mock_job_manager)
|
|
177
|
+
mock_storage.download_file.assert_called()
|
|
178
|
+
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for youtube_service.py - YouTube credential management.
|
|
3
|
+
|
|
4
|
+
These tests mock Secret Manager to verify:
|
|
5
|
+
- Credential loading and parsing
|
|
6
|
+
- Validation of required fields
|
|
7
|
+
- Error handling for missing/invalid credentials
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import pytest
|
|
11
|
+
from unittest.mock import Mock, patch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestYouTubeServiceInit:
|
|
15
|
+
"""Test YouTubeService initialization."""
|
|
16
|
+
|
|
17
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
18
|
+
def test_init_creates_service(self, mock_get_settings):
|
|
19
|
+
"""Test initialization creates service with settings."""
|
|
20
|
+
from backend.services.youtube_service import YouTubeService
|
|
21
|
+
|
|
22
|
+
mock_settings = Mock()
|
|
23
|
+
mock_get_settings.return_value = mock_settings
|
|
24
|
+
|
|
25
|
+
service = YouTubeService()
|
|
26
|
+
|
|
27
|
+
assert service.settings == mock_settings
|
|
28
|
+
assert service._credentials is None
|
|
29
|
+
assert service._loaded is False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestLoadCredentials:
|
|
33
|
+
"""Test load_credentials method."""
|
|
34
|
+
|
|
35
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
36
|
+
def test_load_credentials_success(self, mock_get_settings):
|
|
37
|
+
"""Test successful credential loading from Secret Manager."""
|
|
38
|
+
from backend.services.youtube_service import YouTubeService
|
|
39
|
+
|
|
40
|
+
mock_settings = Mock()
|
|
41
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
42
|
+
"token": "access-token-123",
|
|
43
|
+
"refresh_token": "refresh-token-456",
|
|
44
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
45
|
+
"client_id": "client-id.apps.googleusercontent.com",
|
|
46
|
+
"client_secret": "client-secret-789",
|
|
47
|
+
"scopes": ["https://www.googleapis.com/auth/youtube.upload"],
|
|
48
|
+
})
|
|
49
|
+
mock_get_settings.return_value = mock_settings
|
|
50
|
+
|
|
51
|
+
service = YouTubeService()
|
|
52
|
+
result = service.load_credentials()
|
|
53
|
+
|
|
54
|
+
assert result is True
|
|
55
|
+
assert service._credentials is not None
|
|
56
|
+
assert service._credentials["refresh_token"] == "refresh-token-456"
|
|
57
|
+
mock_settings.get_secret.assert_called_once_with("youtube-oauth-credentials")
|
|
58
|
+
|
|
59
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
60
|
+
def test_load_credentials_not_found(self, mock_get_settings):
|
|
61
|
+
"""Test handling when credentials are not in Secret Manager."""
|
|
62
|
+
from backend.services.youtube_service import YouTubeService
|
|
63
|
+
|
|
64
|
+
mock_settings = Mock()
|
|
65
|
+
mock_settings.get_secret.return_value = None
|
|
66
|
+
mock_get_settings.return_value = mock_settings
|
|
67
|
+
|
|
68
|
+
service = YouTubeService()
|
|
69
|
+
result = service.load_credentials()
|
|
70
|
+
|
|
71
|
+
assert result is False
|
|
72
|
+
assert service._credentials is None
|
|
73
|
+
assert service._loaded is True
|
|
74
|
+
|
|
75
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
76
|
+
def test_load_credentials_missing_required_fields(self, mock_get_settings):
|
|
77
|
+
"""Test handling when credentials are missing required fields."""
|
|
78
|
+
from backend.services.youtube_service import YouTubeService
|
|
79
|
+
|
|
80
|
+
mock_settings = Mock()
|
|
81
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
82
|
+
"token": "access-token",
|
|
83
|
+
# Missing: refresh_token, token_uri, client_id, client_secret
|
|
84
|
+
})
|
|
85
|
+
mock_get_settings.return_value = mock_settings
|
|
86
|
+
|
|
87
|
+
service = YouTubeService()
|
|
88
|
+
result = service.load_credentials()
|
|
89
|
+
|
|
90
|
+
assert result is False
|
|
91
|
+
assert service._credentials is None
|
|
92
|
+
|
|
93
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
94
|
+
def test_load_credentials_invalid_json(self, mock_get_settings):
|
|
95
|
+
"""Test handling of invalid JSON in credentials."""
|
|
96
|
+
from backend.services.youtube_service import YouTubeService
|
|
97
|
+
|
|
98
|
+
mock_settings = Mock()
|
|
99
|
+
mock_settings.get_secret.return_value = "not valid json {"
|
|
100
|
+
mock_get_settings.return_value = mock_settings
|
|
101
|
+
|
|
102
|
+
service = YouTubeService()
|
|
103
|
+
result = service.load_credentials()
|
|
104
|
+
|
|
105
|
+
assert result is False
|
|
106
|
+
assert service._credentials is None
|
|
107
|
+
|
|
108
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
109
|
+
def test_load_credentials_exception(self, mock_get_settings):
|
|
110
|
+
"""Test handling of exceptions during credential loading."""
|
|
111
|
+
from backend.services.youtube_service import YouTubeService
|
|
112
|
+
|
|
113
|
+
mock_settings = Mock()
|
|
114
|
+
mock_settings.get_secret.side_effect = Exception("Secret Manager error")
|
|
115
|
+
mock_get_settings.return_value = mock_settings
|
|
116
|
+
|
|
117
|
+
service = YouTubeService()
|
|
118
|
+
result = service.load_credentials()
|
|
119
|
+
|
|
120
|
+
assert result is False
|
|
121
|
+
assert service._loaded is True
|
|
122
|
+
|
|
123
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
124
|
+
def test_load_credentials_cached(self, mock_get_settings):
|
|
125
|
+
"""Test that credentials are cached after first load."""
|
|
126
|
+
from backend.services.youtube_service import YouTubeService
|
|
127
|
+
|
|
128
|
+
mock_settings = Mock()
|
|
129
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
130
|
+
"refresh_token": "token",
|
|
131
|
+
"token_uri": "uri",
|
|
132
|
+
"client_id": "id",
|
|
133
|
+
"client_secret": "secret",
|
|
134
|
+
})
|
|
135
|
+
mock_get_settings.return_value = mock_settings
|
|
136
|
+
|
|
137
|
+
service = YouTubeService()
|
|
138
|
+
|
|
139
|
+
# First call
|
|
140
|
+
result1 = service.load_credentials()
|
|
141
|
+
# Second call
|
|
142
|
+
result2 = service.load_credentials()
|
|
143
|
+
|
|
144
|
+
assert result1 is True
|
|
145
|
+
assert result2 is True
|
|
146
|
+
# Secret Manager should only be called once
|
|
147
|
+
assert mock_settings.get_secret.call_count == 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestGetCredentialsDict:
|
|
151
|
+
"""Test get_credentials_dict method."""
|
|
152
|
+
|
|
153
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
154
|
+
def test_get_credentials_dict_loads_if_needed(self, mock_get_settings):
|
|
155
|
+
"""Test get_credentials_dict loads credentials if not already loaded."""
|
|
156
|
+
from backend.services.youtube_service import YouTubeService
|
|
157
|
+
|
|
158
|
+
mock_settings = Mock()
|
|
159
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
160
|
+
"refresh_token": "token",
|
|
161
|
+
"token_uri": "uri",
|
|
162
|
+
"client_id": "id",
|
|
163
|
+
"client_secret": "secret",
|
|
164
|
+
})
|
|
165
|
+
mock_get_settings.return_value = mock_settings
|
|
166
|
+
|
|
167
|
+
service = YouTubeService()
|
|
168
|
+
|
|
169
|
+
# Don't explicitly call load_credentials
|
|
170
|
+
result = service.get_credentials_dict()
|
|
171
|
+
|
|
172
|
+
assert result is not None
|
|
173
|
+
assert result["refresh_token"] == "token"
|
|
174
|
+
|
|
175
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
176
|
+
def test_get_credentials_dict_returns_none_if_not_configured(
|
|
177
|
+
self, mock_get_settings
|
|
178
|
+
):
|
|
179
|
+
"""Test get_credentials_dict returns None when not configured."""
|
|
180
|
+
from backend.services.youtube_service import YouTubeService
|
|
181
|
+
|
|
182
|
+
mock_settings = Mock()
|
|
183
|
+
mock_settings.get_secret.return_value = None
|
|
184
|
+
mock_get_settings.return_value = mock_settings
|
|
185
|
+
|
|
186
|
+
service = YouTubeService()
|
|
187
|
+
result = service.get_credentials_dict()
|
|
188
|
+
|
|
189
|
+
assert result is None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestIsConfigured:
|
|
193
|
+
"""Test is_configured property."""
|
|
194
|
+
|
|
195
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
196
|
+
def test_is_configured_true(self, mock_get_settings):
|
|
197
|
+
"""Test is_configured returns True when credentials exist."""
|
|
198
|
+
from backend.services.youtube_service import YouTubeService
|
|
199
|
+
|
|
200
|
+
mock_settings = Mock()
|
|
201
|
+
mock_settings.get_secret.return_value = json.dumps({
|
|
202
|
+
"refresh_token": "token",
|
|
203
|
+
"token_uri": "uri",
|
|
204
|
+
"client_id": "id",
|
|
205
|
+
"client_secret": "secret",
|
|
206
|
+
})
|
|
207
|
+
mock_get_settings.return_value = mock_settings
|
|
208
|
+
|
|
209
|
+
service = YouTubeService()
|
|
210
|
+
|
|
211
|
+
assert service.is_configured is True
|
|
212
|
+
|
|
213
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
214
|
+
def test_is_configured_false(self, mock_get_settings):
|
|
215
|
+
"""Test is_configured returns False when credentials missing."""
|
|
216
|
+
from backend.services.youtube_service import YouTubeService
|
|
217
|
+
|
|
218
|
+
mock_settings = Mock()
|
|
219
|
+
mock_settings.get_secret.return_value = None
|
|
220
|
+
mock_get_settings.return_value = mock_settings
|
|
221
|
+
|
|
222
|
+
service = YouTubeService()
|
|
223
|
+
|
|
224
|
+
assert service.is_configured is False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestGetYouTubeService:
|
|
228
|
+
"""Test get_youtube_service singleton."""
|
|
229
|
+
|
|
230
|
+
@patch("backend.services.youtube_service.get_settings")
|
|
231
|
+
def test_get_youtube_service_singleton(self, mock_get_settings):
|
|
232
|
+
"""Test get_youtube_service returns singleton instance."""
|
|
233
|
+
from backend.services.youtube_service import get_youtube_service
|
|
234
|
+
import backend.services.youtube_service as youtube_module
|
|
235
|
+
|
|
236
|
+
# Reset singleton
|
|
237
|
+
youtube_module._youtube_service = None
|
|
238
|
+
|
|
239
|
+
mock_settings = Mock()
|
|
240
|
+
mock_settings.get_secret.return_value = None
|
|
241
|
+
mock_get_settings.return_value = mock_settings
|
|
242
|
+
|
|
243
|
+
service1 = get_youtube_service()
|
|
244
|
+
service2 = get_youtube_service()
|
|
245
|
+
|
|
246
|
+
assert service1 is service2
|
|
247
|
+
|