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,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for credential manager and auth endpoints.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
import json
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from backend.services.credential_manager import (
|
|
10
|
+
CredentialManager,
|
|
11
|
+
CredentialStatus,
|
|
12
|
+
CredentialCheckResult,
|
|
13
|
+
DeviceAuthInfo,
|
|
14
|
+
get_credential_manager,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestCredentialStatus:
|
|
19
|
+
"""Tests for CredentialStatus enum."""
|
|
20
|
+
|
|
21
|
+
def test_status_values(self):
|
|
22
|
+
"""Test all status values exist."""
|
|
23
|
+
assert CredentialStatus.VALID == "valid"
|
|
24
|
+
assert CredentialStatus.EXPIRED == "expired"
|
|
25
|
+
assert CredentialStatus.INVALID == "invalid"
|
|
26
|
+
assert CredentialStatus.NOT_CONFIGURED == "not_configured"
|
|
27
|
+
assert CredentialStatus.ERROR == "error"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestCredentialCheckResult:
|
|
31
|
+
"""Tests for CredentialCheckResult dataclass."""
|
|
32
|
+
|
|
33
|
+
def test_create_result(self):
|
|
34
|
+
"""Test creating a check result."""
|
|
35
|
+
result = CredentialCheckResult(
|
|
36
|
+
service="youtube",
|
|
37
|
+
status=CredentialStatus.VALID,
|
|
38
|
+
message="Credentials are valid",
|
|
39
|
+
last_checked=datetime.utcnow()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert result.service == "youtube"
|
|
43
|
+
assert result.status == CredentialStatus.VALID
|
|
44
|
+
assert result.message == "Credentials are valid"
|
|
45
|
+
assert result.expires_at is None
|
|
46
|
+
|
|
47
|
+
def test_create_result_with_expiry(self):
|
|
48
|
+
"""Test creating a check result with expiry."""
|
|
49
|
+
expiry = datetime.utcnow()
|
|
50
|
+
result = CredentialCheckResult(
|
|
51
|
+
service="gdrive",
|
|
52
|
+
status=CredentialStatus.VALID,
|
|
53
|
+
message="Valid",
|
|
54
|
+
last_checked=datetime.utcnow(),
|
|
55
|
+
expires_at=expiry
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert result.expires_at == expiry
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestCredentialManager:
|
|
62
|
+
"""Tests for CredentialManager class."""
|
|
63
|
+
|
|
64
|
+
def test_init(self):
|
|
65
|
+
"""Test manager initialization."""
|
|
66
|
+
manager = CredentialManager()
|
|
67
|
+
assert manager._pending_device_auths == {}
|
|
68
|
+
|
|
69
|
+
def test_check_youtube_not_configured(self):
|
|
70
|
+
"""Test YouTube check when not configured."""
|
|
71
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
72
|
+
mock_settings = MagicMock()
|
|
73
|
+
mock_settings.get_secret.return_value = None
|
|
74
|
+
mock_get_settings.return_value = mock_settings
|
|
75
|
+
|
|
76
|
+
manager = CredentialManager()
|
|
77
|
+
result = manager.check_youtube_credentials()
|
|
78
|
+
|
|
79
|
+
assert result.status == CredentialStatus.NOT_CONFIGURED
|
|
80
|
+
assert "not configured" in result.message.lower()
|
|
81
|
+
|
|
82
|
+
def test_check_youtube_invalid_json(self):
|
|
83
|
+
"""Test YouTube check with invalid JSON."""
|
|
84
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
85
|
+
mock_settings = MagicMock()
|
|
86
|
+
mock_settings.get_secret.return_value = "not valid json"
|
|
87
|
+
mock_get_settings.return_value = mock_settings
|
|
88
|
+
|
|
89
|
+
manager = CredentialManager()
|
|
90
|
+
result = manager.check_youtube_credentials()
|
|
91
|
+
|
|
92
|
+
assert result.status == CredentialStatus.INVALID
|
|
93
|
+
assert "json" in result.message.lower()
|
|
94
|
+
|
|
95
|
+
def test_check_youtube_missing_fields(self):
|
|
96
|
+
"""Test YouTube check with missing required fields."""
|
|
97
|
+
# Missing refresh_token and client_secret
|
|
98
|
+
creds = json.dumps({"token": "test", "client_id": "test"})
|
|
99
|
+
|
|
100
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
101
|
+
mock_settings = MagicMock()
|
|
102
|
+
mock_settings.get_secret.return_value = creds
|
|
103
|
+
mock_get_settings.return_value = mock_settings
|
|
104
|
+
|
|
105
|
+
manager = CredentialManager()
|
|
106
|
+
result = manager.check_youtube_credentials()
|
|
107
|
+
|
|
108
|
+
assert result.status == CredentialStatus.INVALID
|
|
109
|
+
assert "missing" in result.message.lower()
|
|
110
|
+
|
|
111
|
+
def test_check_gdrive_not_configured(self):
|
|
112
|
+
"""Test Google Drive check when not configured."""
|
|
113
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
114
|
+
mock_settings = MagicMock()
|
|
115
|
+
mock_settings.get_secret.return_value = None
|
|
116
|
+
mock_get_settings.return_value = mock_settings
|
|
117
|
+
|
|
118
|
+
manager = CredentialManager()
|
|
119
|
+
result = manager.check_gdrive_credentials()
|
|
120
|
+
|
|
121
|
+
assert result.status == CredentialStatus.NOT_CONFIGURED
|
|
122
|
+
|
|
123
|
+
def test_check_dropbox_not_configured(self):
|
|
124
|
+
"""Test Dropbox check when not configured."""
|
|
125
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
126
|
+
mock_settings = MagicMock()
|
|
127
|
+
mock_settings.get_secret.return_value = None
|
|
128
|
+
mock_get_settings.return_value = mock_settings
|
|
129
|
+
|
|
130
|
+
manager = CredentialManager()
|
|
131
|
+
result = manager.check_dropbox_credentials()
|
|
132
|
+
|
|
133
|
+
assert result.status == CredentialStatus.NOT_CONFIGURED
|
|
134
|
+
|
|
135
|
+
def test_check_dropbox_missing_access_token(self):
|
|
136
|
+
"""Test Dropbox check with missing access token."""
|
|
137
|
+
creds = json.dumps({"refresh_token": "test"}) # No access_token
|
|
138
|
+
|
|
139
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
140
|
+
mock_settings = MagicMock()
|
|
141
|
+
mock_settings.get_secret.return_value = creds
|
|
142
|
+
mock_get_settings.return_value = mock_settings
|
|
143
|
+
|
|
144
|
+
manager = CredentialManager()
|
|
145
|
+
result = manager.check_dropbox_credentials()
|
|
146
|
+
|
|
147
|
+
assert result.status == CredentialStatus.INVALID
|
|
148
|
+
assert "access_token" in result.message.lower()
|
|
149
|
+
|
|
150
|
+
def test_check_all_credentials(self):
|
|
151
|
+
"""Test checking all credentials."""
|
|
152
|
+
with patch('backend.services.credential_manager.get_settings') as mock_get_settings:
|
|
153
|
+
mock_settings = MagicMock()
|
|
154
|
+
mock_settings.get_secret.return_value = None
|
|
155
|
+
mock_get_settings.return_value = mock_settings
|
|
156
|
+
|
|
157
|
+
manager = CredentialManager()
|
|
158
|
+
results = manager.check_all_credentials()
|
|
159
|
+
|
|
160
|
+
assert "youtube" in results
|
|
161
|
+
assert "gdrive" in results
|
|
162
|
+
assert "dropbox" in results
|
|
163
|
+
assert all(r.status == CredentialStatus.NOT_CONFIGURED for r in results.values())
|
|
164
|
+
|
|
165
|
+
def test_test_youtube_api_failure(self):
|
|
166
|
+
"""Test YouTube API test failure."""
|
|
167
|
+
manager = CredentialManager()
|
|
168
|
+
|
|
169
|
+
mock_creds = MagicMock()
|
|
170
|
+
|
|
171
|
+
with patch('googleapiclient.discovery.build', side_effect=Exception("API Error")):
|
|
172
|
+
result = manager._test_youtube_api(mock_creds)
|
|
173
|
+
|
|
174
|
+
assert result is False
|
|
175
|
+
|
|
176
|
+
def test_test_gdrive_api_failure(self):
|
|
177
|
+
"""Test Google Drive API test failure."""
|
|
178
|
+
manager = CredentialManager()
|
|
179
|
+
|
|
180
|
+
mock_creds = MagicMock()
|
|
181
|
+
|
|
182
|
+
with patch('googleapiclient.discovery.build', side_effect=Exception("API Error")):
|
|
183
|
+
result = manager._test_gdrive_api(mock_creds)
|
|
184
|
+
|
|
185
|
+
assert result is False
|
|
186
|
+
|
|
187
|
+
def test_test_dropbox_api_failure(self):
|
|
188
|
+
"""Test Dropbox API test failure."""
|
|
189
|
+
manager = CredentialManager()
|
|
190
|
+
|
|
191
|
+
with patch('dropbox.Dropbox', side_effect=Exception("API Error")):
|
|
192
|
+
result = manager._test_dropbox_api({"access_token": "test"})
|
|
193
|
+
|
|
194
|
+
assert result is False
|
|
195
|
+
|
|
196
|
+
def test_send_credential_alert_no_webhook(self):
|
|
197
|
+
"""Test alert without webhook URL."""
|
|
198
|
+
manager = CredentialManager()
|
|
199
|
+
|
|
200
|
+
result = manager.send_credential_alert([], discord_webhook_url=None)
|
|
201
|
+
|
|
202
|
+
assert result is False
|
|
203
|
+
|
|
204
|
+
def test_send_credential_alert_success(self):
|
|
205
|
+
"""Test successful alert sending."""
|
|
206
|
+
manager = CredentialManager()
|
|
207
|
+
|
|
208
|
+
mock_response = MagicMock()
|
|
209
|
+
mock_response.status_code = 200
|
|
210
|
+
|
|
211
|
+
invalid_services = [
|
|
212
|
+
CredentialCheckResult(
|
|
213
|
+
service="youtube",
|
|
214
|
+
status=CredentialStatus.INVALID,
|
|
215
|
+
message="Test message",
|
|
216
|
+
last_checked=datetime.utcnow()
|
|
217
|
+
)
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
with patch('requests.post', return_value=mock_response) as mock_post:
|
|
221
|
+
result = manager.send_credential_alert(
|
|
222
|
+
invalid_services,
|
|
223
|
+
discord_webhook_url="https://discord.com/webhook/test"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert result is True
|
|
227
|
+
mock_post.assert_called_once()
|
|
228
|
+
|
|
229
|
+
def test_start_youtube_device_auth(self):
|
|
230
|
+
"""Test starting YouTube device auth flow."""
|
|
231
|
+
manager = CredentialManager()
|
|
232
|
+
|
|
233
|
+
mock_response = MagicMock()
|
|
234
|
+
mock_response.status_code = 200
|
|
235
|
+
mock_response.json.return_value = {
|
|
236
|
+
"device_code": "test_device_code",
|
|
237
|
+
"user_code": "TEST-CODE",
|
|
238
|
+
"verification_uri": "https://google.com/device",
|
|
239
|
+
"expires_in": 1800,
|
|
240
|
+
"interval": 5
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
with patch('requests.post', return_value=mock_response):
|
|
244
|
+
device_info = manager.start_youtube_device_auth(
|
|
245
|
+
client_id="test_client",
|
|
246
|
+
client_secret="test_secret"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
assert device_info.device_code == "test_device_code"
|
|
250
|
+
assert device_info.user_code == "TEST-CODE"
|
|
251
|
+
assert device_info.verification_url == "https://google.com/device"
|
|
252
|
+
|
|
253
|
+
# Should be stored for polling
|
|
254
|
+
key = f"youtube:{device_info.device_code}"
|
|
255
|
+
assert key in manager._pending_device_auths
|
|
256
|
+
|
|
257
|
+
def test_poll_device_auth_no_client_creds(self):
|
|
258
|
+
"""Test polling when client credentials are not in Secret Manager."""
|
|
259
|
+
manager = CredentialManager()
|
|
260
|
+
|
|
261
|
+
# Mock get_youtube_client_credentials to return None (no creds)
|
|
262
|
+
with patch.object(manager, 'get_youtube_client_credentials', return_value=None):
|
|
263
|
+
status, data = manager.poll_device_auth("youtube", "some_code")
|
|
264
|
+
|
|
265
|
+
assert status == "error"
|
|
266
|
+
assert "client credentials not found" in data["message"].lower()
|
|
267
|
+
|
|
268
|
+
def test_poll_device_auth_expired(self):
|
|
269
|
+
"""Test polling with expired device code."""
|
|
270
|
+
manager = CredentialManager()
|
|
271
|
+
|
|
272
|
+
mock_response = MagicMock()
|
|
273
|
+
mock_response.status_code = 400
|
|
274
|
+
mock_response.json.return_value = {"error": "expired_token"}
|
|
275
|
+
|
|
276
|
+
with patch.object(manager, 'get_youtube_client_credentials',
|
|
277
|
+
return_value={"client_id": "test", "client_secret": "test"}):
|
|
278
|
+
with patch('requests.post', return_value=mock_response):
|
|
279
|
+
status, data = manager.poll_device_auth("youtube", "expired_code")
|
|
280
|
+
|
|
281
|
+
assert status == "expired"
|
|
282
|
+
assert "expired" in data["message"].lower()
|
|
283
|
+
|
|
284
|
+
def test_poll_device_auth_pending(self):
|
|
285
|
+
"""Test polling when authorization is pending."""
|
|
286
|
+
manager = CredentialManager()
|
|
287
|
+
|
|
288
|
+
mock_response = MagicMock()
|
|
289
|
+
mock_response.status_code = 400
|
|
290
|
+
mock_response.json.return_value = {"error": "authorization_pending"}
|
|
291
|
+
|
|
292
|
+
with patch.object(manager, 'get_youtube_client_credentials',
|
|
293
|
+
return_value={"client_id": "test", "client_secret": "test"}):
|
|
294
|
+
with patch('requests.post', return_value=mock_response):
|
|
295
|
+
status, data = manager.poll_device_auth("youtube", "pending_code")
|
|
296
|
+
|
|
297
|
+
assert status == "pending"
|
|
298
|
+
assert "waiting" in data["message"].lower()
|
|
299
|
+
|
|
300
|
+
def test_poll_device_auth_success(self):
|
|
301
|
+
"""Test polling when authorization completes successfully."""
|
|
302
|
+
manager = CredentialManager()
|
|
303
|
+
|
|
304
|
+
mock_response = MagicMock()
|
|
305
|
+
mock_response.status_code = 200
|
|
306
|
+
mock_response.json.return_value = {
|
|
307
|
+
"access_token": "new_access_token",
|
|
308
|
+
"refresh_token": "new_refresh_token"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
with patch.object(manager, 'get_youtube_client_credentials',
|
|
312
|
+
return_value={"client_id": "test_id", "client_secret": "test_secret"}):
|
|
313
|
+
with patch('requests.post', return_value=mock_response):
|
|
314
|
+
with patch.object(manager, '_save_credentials_to_secret', return_value=True):
|
|
315
|
+
status, data = manager.poll_device_auth("youtube", "completed_code")
|
|
316
|
+
|
|
317
|
+
assert status == "complete"
|
|
318
|
+
assert data["token"] == "new_access_token"
|
|
319
|
+
assert data["refresh_token"] == "new_refresh_token"
|
|
320
|
+
assert data["client_id"] == "test_id"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class TestGetCredentialManager:
|
|
324
|
+
"""Tests for singleton factory function."""
|
|
325
|
+
|
|
326
|
+
def test_returns_singleton(self):
|
|
327
|
+
"""Test that factory returns same instance."""
|
|
328
|
+
# Reset singleton
|
|
329
|
+
import backend.services.credential_manager as module
|
|
330
|
+
module._credential_manager = None
|
|
331
|
+
|
|
332
|
+
manager1 = get_credential_manager()
|
|
333
|
+
manager2 = get_credential_manager()
|
|
334
|
+
|
|
335
|
+
assert manager1 is manager2
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TestAuthRoutes:
|
|
339
|
+
"""Tests for auth API routes."""
|
|
340
|
+
|
|
341
|
+
def test_routes_import(self):
|
|
342
|
+
"""Test that routes can be imported."""
|
|
343
|
+
from backend.api.routes.auth import router
|
|
344
|
+
assert router is not None
|
|
345
|
+
|
|
346
|
+
def test_status_endpoint_exists(self):
|
|
347
|
+
"""Test that status endpoint is defined."""
|
|
348
|
+
from backend.api.routes.auth import get_credentials_status
|
|
349
|
+
assert get_credentials_status is not None
|
|
350
|
+
|
|
351
|
+
def test_validate_endpoint_exists(self):
|
|
352
|
+
"""Test that validate endpoint is defined."""
|
|
353
|
+
from backend.api.routes.auth import validate_credentials
|
|
354
|
+
assert validate_credentials is not None
|
|
355
|
+
|
|
356
|
+
def test_device_auth_endpoint_exists(self):
|
|
357
|
+
"""Test that device auth endpoints are defined."""
|
|
358
|
+
from backend.api.routes.auth import (
|
|
359
|
+
start_youtube_device_auth,
|
|
360
|
+
poll_youtube_device_auth,
|
|
361
|
+
start_gdrive_device_auth,
|
|
362
|
+
poll_gdrive_device_auth,
|
|
363
|
+
)
|
|
364
|
+
assert start_youtube_device_auth is not None
|
|
365
|
+
assert poll_youtube_device_auth is not None
|
|
366
|
+
assert start_gdrive_device_auth is not None
|
|
367
|
+
assert poll_gdrive_device_auth is not None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestFileUploadCredentialValidation:
|
|
371
|
+
"""Tests for credential validation in file upload."""
|
|
372
|
+
|
|
373
|
+
def test_credential_manager_imported(self):
|
|
374
|
+
"""Test that credential manager is imported in file upload."""
|
|
375
|
+
from backend.api.routes.file_upload import get_credential_manager, CredentialStatus
|
|
376
|
+
assert get_credential_manager is not None
|
|
377
|
+
assert CredentialStatus is not None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for API dependencies (authentication, authorization).
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestAuthDependencies:
|
|
9
|
+
"""Tests for authentication dependencies module structure."""
|
|
10
|
+
|
|
11
|
+
def test_get_token_from_request_function_exists(self):
|
|
12
|
+
"""Test get_token_from_request function exists."""
|
|
13
|
+
from backend.api.dependencies import get_token_from_request
|
|
14
|
+
assert get_token_from_request is not None
|
|
15
|
+
|
|
16
|
+
def test_require_auth_function_exists(self):
|
|
17
|
+
"""Test require_auth function exists."""
|
|
18
|
+
from backend.api.dependencies import require_auth
|
|
19
|
+
assert require_auth is not None
|
|
20
|
+
|
|
21
|
+
def test_require_admin_function_exists(self):
|
|
22
|
+
"""Test require_admin function exists."""
|
|
23
|
+
from backend.api.dependencies import require_admin
|
|
24
|
+
assert require_admin is not None
|
|
25
|
+
|
|
26
|
+
def test_optional_auth_function_exists(self):
|
|
27
|
+
"""Test optional_auth function exists."""
|
|
28
|
+
from backend.api.dependencies import optional_auth
|
|
29
|
+
assert optional_auth is not None
|
|
30
|
+
|
|
31
|
+
def test_security_scheme_defined(self):
|
|
32
|
+
"""Test HTTP Bearer security scheme is defined."""
|
|
33
|
+
from backend.api.dependencies import security
|
|
34
|
+
assert security is not None
|
|
35
|
+
|
|
36
|
+
def test_logger_configured(self):
|
|
37
|
+
"""Test logger is configured."""
|
|
38
|
+
from backend.api.dependencies import logger
|
|
39
|
+
assert logger is not None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestAuthServiceAccess:
|
|
43
|
+
"""Tests for auth service access."""
|
|
44
|
+
|
|
45
|
+
def test_get_auth_service_function_exists(self):
|
|
46
|
+
"""Test get_auth_service function exists."""
|
|
47
|
+
from backend.services.auth_service import get_auth_service
|
|
48
|
+
assert get_auth_service is not None
|
|
49
|
+
|
|
50
|
+
def test_user_type_enum_defined(self):
|
|
51
|
+
"""Test UserType enum is defined."""
|
|
52
|
+
from backend.services.auth_service import UserType
|
|
53
|
+
assert UserType is not None
|
|
54
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for DiscordNotificationService.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Service initialization
|
|
6
|
+
- Webhook URL validation
|
|
7
|
+
- Message posting
|
|
8
|
+
- Video notification posting
|
|
9
|
+
- Dry run mode
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from unittest.mock import MagicMock, patch
|
|
14
|
+
|
|
15
|
+
from backend.services.discord_service import (
|
|
16
|
+
DiscordNotificationService,
|
|
17
|
+
get_discord_notification_service,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestDiscordNotificationServiceInit:
|
|
22
|
+
"""Test service initialization."""
|
|
23
|
+
|
|
24
|
+
def test_init_with_webhook_url(self):
|
|
25
|
+
"""Test initialization with valid webhook URL."""
|
|
26
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef123456"
|
|
27
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
28
|
+
assert service.webhook_url == url
|
|
29
|
+
assert service.is_enabled() is True
|
|
30
|
+
|
|
31
|
+
def test_init_without_webhook_url(self):
|
|
32
|
+
"""Test initialization without webhook URL."""
|
|
33
|
+
service = DiscordNotificationService()
|
|
34
|
+
assert service.webhook_url is None
|
|
35
|
+
assert service.is_enabled() is False
|
|
36
|
+
|
|
37
|
+
def test_init_with_dry_run(self):
|
|
38
|
+
"""Test initialization with dry run mode."""
|
|
39
|
+
service = DiscordNotificationService(dry_run=True)
|
|
40
|
+
assert service.dry_run is True
|
|
41
|
+
|
|
42
|
+
def test_init_with_invalid_webhook_url_raises(self):
|
|
43
|
+
"""Test that invalid webhook URL raises ValueError."""
|
|
44
|
+
with pytest.raises(ValueError) as exc_info:
|
|
45
|
+
DiscordNotificationService(webhook_url="https://example.com/webhook")
|
|
46
|
+
assert "Invalid Discord webhook URL" in str(exc_info.value)
|
|
47
|
+
|
|
48
|
+
def test_init_with_non_discord_url_raises(self):
|
|
49
|
+
"""Test that non-Discord URL raises ValueError."""
|
|
50
|
+
with pytest.raises(ValueError):
|
|
51
|
+
DiscordNotificationService(
|
|
52
|
+
webhook_url="https://slack.com/api/webhooks/123"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestDiscordNotificationServiceValidation:
|
|
57
|
+
"""Test webhook URL validation."""
|
|
58
|
+
|
|
59
|
+
def test_validate_valid_webhook_url(self):
|
|
60
|
+
"""Test that valid webhook URL passes validation."""
|
|
61
|
+
service = DiscordNotificationService()
|
|
62
|
+
# Should not raise
|
|
63
|
+
service._validate_webhook_url(
|
|
64
|
+
"https://discord.com/api/webhooks/123456789/abcdef"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_validate_invalid_webhook_url(self):
|
|
68
|
+
"""Test that invalid webhook URL raises ValueError."""
|
|
69
|
+
service = DiscordNotificationService()
|
|
70
|
+
with pytest.raises(ValueError) as exc_info:
|
|
71
|
+
service._validate_webhook_url("https://example.com/webhook")
|
|
72
|
+
assert "Invalid Discord webhook URL" in str(exc_info.value)
|
|
73
|
+
|
|
74
|
+
def test_validate_strips_whitespace(self):
|
|
75
|
+
"""Test that whitespace is stripped from URL."""
|
|
76
|
+
service = DiscordNotificationService()
|
|
77
|
+
# Should not raise - whitespace is stripped
|
|
78
|
+
service._validate_webhook_url(
|
|
79
|
+
" https://discord.com/api/webhooks/123456789/abcdef "
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestDiscordNotificationServicePostMessage:
|
|
84
|
+
"""Test message posting."""
|
|
85
|
+
|
|
86
|
+
@patch("backend.services.discord_service.requests.post")
|
|
87
|
+
def test_post_message_success(self, mock_post):
|
|
88
|
+
"""Test successful message posting."""
|
|
89
|
+
mock_response = MagicMock()
|
|
90
|
+
mock_response.raise_for_status = MagicMock()
|
|
91
|
+
mock_post.return_value = mock_response
|
|
92
|
+
|
|
93
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
94
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
95
|
+
|
|
96
|
+
result = service.post_message("Test message")
|
|
97
|
+
|
|
98
|
+
assert result is True
|
|
99
|
+
mock_post.assert_called_once_with(
|
|
100
|
+
url,
|
|
101
|
+
json={"content": "Test message"},
|
|
102
|
+
timeout=30
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@patch("backend.services.discord_service.requests.post")
|
|
106
|
+
def test_post_message_with_custom_webhook(self, mock_post):
|
|
107
|
+
"""Test message posting with custom webhook URL."""
|
|
108
|
+
mock_response = MagicMock()
|
|
109
|
+
mock_response.raise_for_status = MagicMock()
|
|
110
|
+
mock_post.return_value = mock_response
|
|
111
|
+
|
|
112
|
+
service = DiscordNotificationService()
|
|
113
|
+
custom_url = "https://discord.com/api/webhooks/987654321/fedcba"
|
|
114
|
+
|
|
115
|
+
result = service.post_message("Test message", webhook_url=custom_url)
|
|
116
|
+
|
|
117
|
+
assert result is True
|
|
118
|
+
mock_post.assert_called_once_with(
|
|
119
|
+
custom_url,
|
|
120
|
+
json={"content": "Test message"},
|
|
121
|
+
timeout=30
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def test_post_message_no_webhook_raises(self):
|
|
125
|
+
"""Test that posting without webhook URL raises ValueError."""
|
|
126
|
+
service = DiscordNotificationService()
|
|
127
|
+
|
|
128
|
+
with pytest.raises(ValueError) as exc_info:
|
|
129
|
+
service.post_message("Test message")
|
|
130
|
+
assert "No Discord webhook URL provided" in str(exc_info.value)
|
|
131
|
+
|
|
132
|
+
def test_post_message_dry_run(self):
|
|
133
|
+
"""Test message posting in dry run mode."""
|
|
134
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
135
|
+
service = DiscordNotificationService(webhook_url=url, dry_run=True)
|
|
136
|
+
|
|
137
|
+
result = service.post_message("Test message")
|
|
138
|
+
|
|
139
|
+
assert result is True
|
|
140
|
+
# In dry run mode, no actual request should be made
|
|
141
|
+
|
|
142
|
+
@patch("backend.services.discord_service.requests.post")
|
|
143
|
+
def test_post_message_http_error(self, mock_post):
|
|
144
|
+
"""Test handling of HTTP errors."""
|
|
145
|
+
import requests as req
|
|
146
|
+
mock_response = MagicMock()
|
|
147
|
+
mock_response.raise_for_status.side_effect = req.HTTPError("404 Not Found")
|
|
148
|
+
mock_post.return_value = mock_response
|
|
149
|
+
|
|
150
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
151
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
152
|
+
|
|
153
|
+
with pytest.raises(req.HTTPError):
|
|
154
|
+
service.post_message("Test message")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestDiscordNotificationServiceVideoNotification:
|
|
158
|
+
"""Test video notification posting."""
|
|
159
|
+
|
|
160
|
+
@patch("backend.services.discord_service.requests.post")
|
|
161
|
+
def test_post_video_notification_success(self, mock_post):
|
|
162
|
+
"""Test successful video notification."""
|
|
163
|
+
mock_response = MagicMock()
|
|
164
|
+
mock_response.raise_for_status = MagicMock()
|
|
165
|
+
mock_post.return_value = mock_response
|
|
166
|
+
|
|
167
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
168
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
169
|
+
|
|
170
|
+
result = service.post_video_notification(
|
|
171
|
+
"https://www.youtube.com/watch?v=abc123"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert result is True
|
|
175
|
+
mock_post.assert_called_once()
|
|
176
|
+
call_args = mock_post.call_args
|
|
177
|
+
assert "New upload:" in call_args[1]["json"]["content"]
|
|
178
|
+
assert "abc123" in call_args[1]["json"]["content"]
|
|
179
|
+
|
|
180
|
+
def test_post_video_notification_no_youtube_url(self):
|
|
181
|
+
"""Test video notification with empty YouTube URL."""
|
|
182
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
183
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
184
|
+
|
|
185
|
+
result = service.post_video_notification("")
|
|
186
|
+
|
|
187
|
+
assert result is False
|
|
188
|
+
|
|
189
|
+
def test_post_video_notification_none_youtube_url(self):
|
|
190
|
+
"""Test video notification with None YouTube URL."""
|
|
191
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
192
|
+
service = DiscordNotificationService(webhook_url=url)
|
|
193
|
+
|
|
194
|
+
result = service.post_video_notification(None)
|
|
195
|
+
|
|
196
|
+
assert result is False
|
|
197
|
+
|
|
198
|
+
def test_post_video_notification_dry_run(self):
|
|
199
|
+
"""Test video notification in dry run mode."""
|
|
200
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
201
|
+
service = DiscordNotificationService(webhook_url=url, dry_run=True)
|
|
202
|
+
|
|
203
|
+
result = service.post_video_notification(
|
|
204
|
+
"https://www.youtube.com/watch?v=abc123"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert result is True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestGetDiscordNotificationService:
|
|
211
|
+
"""Test factory function."""
|
|
212
|
+
|
|
213
|
+
def test_get_service_creates_instance(self):
|
|
214
|
+
"""Test that factory function creates a new instance."""
|
|
215
|
+
import backend.services.discord_service as module
|
|
216
|
+
module._discord_notification_service = None
|
|
217
|
+
|
|
218
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
219
|
+
service = get_discord_notification_service(webhook_url=url)
|
|
220
|
+
|
|
221
|
+
assert service is not None
|
|
222
|
+
assert isinstance(service, DiscordNotificationService)
|
|
223
|
+
assert service.webhook_url == url
|
|
224
|
+
|
|
225
|
+
def test_get_service_without_webhook(self):
|
|
226
|
+
"""Test factory function without webhook URL."""
|
|
227
|
+
import backend.services.discord_service as module
|
|
228
|
+
module._discord_notification_service = None
|
|
229
|
+
|
|
230
|
+
service = get_discord_notification_service()
|
|
231
|
+
|
|
232
|
+
assert service is not None
|
|
233
|
+
assert service.webhook_url is None
|
|
234
|
+
assert service.is_enabled() is False
|
|
235
|
+
|
|
236
|
+
def test_get_service_with_dry_run(self):
|
|
237
|
+
"""Test factory function with dry run mode."""
|
|
238
|
+
import backend.services.discord_service as module
|
|
239
|
+
module._discord_notification_service = None
|
|
240
|
+
|
|
241
|
+
url = "https://discord.com/api/webhooks/123456789/abcdef"
|
|
242
|
+
service = get_discord_notification_service(webhook_url=url, dry_run=True)
|
|
243
|
+
|
|
244
|
+
assert service.dry_run is True
|