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,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for file_upload.py API routes using FastAPI TestClient.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from datetime import datetime, UTC
|
|
6
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
7
|
+
from fastapi.testclient import TestClient
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
|
|
10
|
+
from backend.models.job import Job, JobStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def upload_auth_headers():
|
|
15
|
+
"""Auth headers for file upload tests (no Content-Type, let multipart work)."""
|
|
16
|
+
return {
|
|
17
|
+
"Authorization": "Bearer test-admin-token"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_job():
|
|
23
|
+
"""Create a standard mock job."""
|
|
24
|
+
return Job(
|
|
25
|
+
job_id="test123",
|
|
26
|
+
status=JobStatus.PENDING,
|
|
27
|
+
created_at=datetime.now(UTC),
|
|
28
|
+
updated_at=datetime.now(UTC),
|
|
29
|
+
artist="Test Artist",
|
|
30
|
+
title="Test Song",
|
|
31
|
+
input_media_gcs_path="uploads/test123/song.flac"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_services(mock_job):
|
|
37
|
+
"""Create all mocked services needed for upload."""
|
|
38
|
+
mock_job_manager = MagicMock()
|
|
39
|
+
mock_job_manager.create_job.return_value = mock_job
|
|
40
|
+
mock_job_manager.get_job.return_value = mock_job
|
|
41
|
+
mock_job_manager.update_job.return_value = None
|
|
42
|
+
|
|
43
|
+
mock_storage = MagicMock()
|
|
44
|
+
mock_storage.upload_fileobj.return_value = "gs://bucket/uploads/test123/song.flac"
|
|
45
|
+
|
|
46
|
+
mock_worker_service = MagicMock()
|
|
47
|
+
mock_worker_service.trigger_audio_worker = AsyncMock(return_value=True)
|
|
48
|
+
mock_worker_service.trigger_lyrics_worker = AsyncMock(return_value=True)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
'job_manager': mock_job_manager,
|
|
52
|
+
'storage': mock_storage,
|
|
53
|
+
'worker_service': mock_worker_service
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def client(mock_services):
|
|
59
|
+
"""Create TestClient with mocked dependencies."""
|
|
60
|
+
mock_creds = MagicMock()
|
|
61
|
+
mock_creds.universe_domain = 'googleapis.com'
|
|
62
|
+
|
|
63
|
+
with patch('backend.api.routes.file_upload.job_manager', mock_services['job_manager']), \
|
|
64
|
+
patch('backend.api.routes.file_upload.storage_service', mock_services['storage']), \
|
|
65
|
+
patch('backend.api.routes.file_upload.worker_service', mock_services['worker_service']), \
|
|
66
|
+
patch('backend.services.firestore_service.firestore'), \
|
|
67
|
+
patch('backend.services.storage_service.storage'), \
|
|
68
|
+
patch('google.auth.default', return_value=(mock_creds, 'test-project')):
|
|
69
|
+
from backend.main import app
|
|
70
|
+
yield TestClient(app)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestFileUploadEndpoint:
|
|
74
|
+
"""Tests for POST /api/jobs/upload."""
|
|
75
|
+
|
|
76
|
+
def test_upload_flac_returns_200(self, client, mock_services, upload_auth_headers):
|
|
77
|
+
"""Test uploading FLAC file returns 200."""
|
|
78
|
+
response = client.post(
|
|
79
|
+
"/api/jobs/upload",
|
|
80
|
+
headers=upload_auth_headers,
|
|
81
|
+
files={"file": ("test.flac", BytesIO(b"fake audio"), "audio/flac")},
|
|
82
|
+
data={"artist": "Test Artist", "title": "Test Song"}
|
|
83
|
+
)
|
|
84
|
+
assert response.status_code == 200
|
|
85
|
+
|
|
86
|
+
def test_upload_mp3_returns_200(self, client, mock_services, upload_auth_headers):
|
|
87
|
+
"""Test uploading MP3 file returns 200."""
|
|
88
|
+
response = client.post(
|
|
89
|
+
"/api/jobs/upload",
|
|
90
|
+
headers=upload_auth_headers,
|
|
91
|
+
files={"file": ("test.mp3", BytesIO(b"fake audio"), "audio/mpeg")},
|
|
92
|
+
data={"artist": "Test Artist", "title": "Test Song"}
|
|
93
|
+
)
|
|
94
|
+
assert response.status_code == 200
|
|
95
|
+
|
|
96
|
+
def test_upload_wav_returns_200(self, client, mock_services, upload_auth_headers):
|
|
97
|
+
"""Test uploading WAV file returns 200."""
|
|
98
|
+
response = client.post(
|
|
99
|
+
"/api/jobs/upload",
|
|
100
|
+
headers=upload_auth_headers,
|
|
101
|
+
files={"file": ("test.wav", BytesIO(b"fake audio"), "audio/wav")},
|
|
102
|
+
data={"artist": "Test Artist", "title": "Test Song"}
|
|
103
|
+
)
|
|
104
|
+
assert response.status_code == 200
|
|
105
|
+
|
|
106
|
+
def test_upload_returns_job_id(self, client, mock_services, upload_auth_headers):
|
|
107
|
+
"""Test upload response contains job_id."""
|
|
108
|
+
response = client.post(
|
|
109
|
+
"/api/jobs/upload",
|
|
110
|
+
headers=upload_auth_headers,
|
|
111
|
+
files={"file": ("test.flac", BytesIO(b"fake audio"), "audio/flac")},
|
|
112
|
+
data={"artist": "Test", "title": "Song"}
|
|
113
|
+
)
|
|
114
|
+
data = response.json()
|
|
115
|
+
assert "job_id" in data
|
|
116
|
+
assert data["status"] == "success"
|
|
117
|
+
|
|
118
|
+
def test_upload_returns_filename(self, client, mock_services, upload_auth_headers):
|
|
119
|
+
"""Test upload response contains original filename."""
|
|
120
|
+
response = client.post(
|
|
121
|
+
"/api/jobs/upload",
|
|
122
|
+
headers=upload_auth_headers,
|
|
123
|
+
files={"file": ("my_song.flac", BytesIO(b"fake audio"), "audio/flac")},
|
|
124
|
+
data={"artist": "Test", "title": "Song"}
|
|
125
|
+
)
|
|
126
|
+
data = response.json()
|
|
127
|
+
assert "filename" in data
|
|
128
|
+
assert data["filename"] == "my_song.flac"
|
|
129
|
+
|
|
130
|
+
def test_upload_rejects_txt_file(self, client, mock_services, upload_auth_headers):
|
|
131
|
+
"""Test upload rejects text files."""
|
|
132
|
+
response = client.post(
|
|
133
|
+
"/api/jobs/upload",
|
|
134
|
+
headers=upload_auth_headers,
|
|
135
|
+
files={"file": ("test.txt", BytesIO(b"not audio"), "text/plain")},
|
|
136
|
+
data={"artist": "Test", "title": "Song"}
|
|
137
|
+
)
|
|
138
|
+
assert response.status_code == 400
|
|
139
|
+
|
|
140
|
+
def test_upload_rejects_pdf_file(self, client, mock_services, upload_auth_headers):
|
|
141
|
+
"""Test upload rejects PDF files."""
|
|
142
|
+
response = client.post(
|
|
143
|
+
"/api/jobs/upload",
|
|
144
|
+
headers=upload_auth_headers,
|
|
145
|
+
files={"file": ("doc.pdf", BytesIO(b"pdf content"), "application/pdf")},
|
|
146
|
+
data={"artist": "Test", "title": "Song"}
|
|
147
|
+
)
|
|
148
|
+
assert response.status_code == 400
|
|
149
|
+
|
|
150
|
+
def test_upload_rejects_exe_file(self, client, mock_services, upload_auth_headers):
|
|
151
|
+
"""Test upload rejects executable files."""
|
|
152
|
+
response = client.post(
|
|
153
|
+
"/api/jobs/upload",
|
|
154
|
+
headers=upload_auth_headers,
|
|
155
|
+
files={"file": ("app.exe", BytesIO(b"exe content"), "application/octet-stream")},
|
|
156
|
+
data={"artist": "Test", "title": "Song"}
|
|
157
|
+
)
|
|
158
|
+
assert response.status_code == 400
|
|
159
|
+
|
|
160
|
+
def test_upload_requires_artist(self, client, mock_services, upload_auth_headers):
|
|
161
|
+
"""Test upload requires artist field."""
|
|
162
|
+
response = client.post(
|
|
163
|
+
"/api/jobs/upload",
|
|
164
|
+
headers=upload_auth_headers,
|
|
165
|
+
files={"file": ("test.flac", BytesIO(b"audio"), "audio/flac")},
|
|
166
|
+
data={"title": "Song"} # Missing artist
|
|
167
|
+
)
|
|
168
|
+
assert response.status_code == 422
|
|
169
|
+
|
|
170
|
+
def test_upload_requires_title(self, client, mock_services, upload_auth_headers):
|
|
171
|
+
"""Test upload requires title field."""
|
|
172
|
+
response = client.post(
|
|
173
|
+
"/api/jobs/upload",
|
|
174
|
+
headers=upload_auth_headers,
|
|
175
|
+
files={"file": ("test.flac", BytesIO(b"audio"), "audio/flac")},
|
|
176
|
+
data={"artist": "Artist"} # Missing title
|
|
177
|
+
)
|
|
178
|
+
assert response.status_code == 422
|
|
179
|
+
|
|
180
|
+
def test_upload_requires_file(self, client, mock_services, upload_auth_headers):
|
|
181
|
+
"""Test upload requires file."""
|
|
182
|
+
response = client.post(
|
|
183
|
+
"/api/jobs/upload",
|
|
184
|
+
headers=upload_auth_headers,
|
|
185
|
+
data={"artist": "Artist", "title": "Song"}
|
|
186
|
+
# Missing file
|
|
187
|
+
)
|
|
188
|
+
assert response.status_code == 422
|
|
189
|
+
|
|
190
|
+
def test_upload_triggers_workers(self, client, mock_services, upload_auth_headers):
|
|
191
|
+
"""Test upload triggers audio and lyrics workers."""
|
|
192
|
+
response = client.post(
|
|
193
|
+
"/api/jobs/upload",
|
|
194
|
+
headers=upload_auth_headers,
|
|
195
|
+
files={"file": ("test.flac", BytesIO(b"audio"), "audio/flac")},
|
|
196
|
+
data={"artist": "Test", "title": "Song"}
|
|
197
|
+
)
|
|
198
|
+
assert response.status_code == 200
|
|
199
|
+
# Workers should be triggered in background
|
|
200
|
+
|
|
201
|
+
def test_upload_creates_job(self, client, mock_services, upload_auth_headers):
|
|
202
|
+
"""Test upload creates job in job manager."""
|
|
203
|
+
response = client.post(
|
|
204
|
+
"/api/jobs/upload",
|
|
205
|
+
headers=upload_auth_headers,
|
|
206
|
+
files={"file": ("test.flac", BytesIO(b"audio"), "audio/flac")},
|
|
207
|
+
data={"artist": "Test", "title": "Song"}
|
|
208
|
+
)
|
|
209
|
+
assert response.status_code == 200
|
|
210
|
+
mock_services['job_manager'].create_job.assert_called()
|
|
211
|
+
|
|
212
|
+
def test_upload_stores_file_to_gcs(self, client, mock_services, upload_auth_headers):
|
|
213
|
+
"""Test upload stores file to GCS."""
|
|
214
|
+
response = client.post(
|
|
215
|
+
"/api/jobs/upload",
|
|
216
|
+
headers=upload_auth_headers,
|
|
217
|
+
files={"file": ("test.flac", BytesIO(b"audio"), "audio/flac")},
|
|
218
|
+
data={"artist": "Test", "title": "Song"}
|
|
219
|
+
)
|
|
220
|
+
assert response.status_code == 200
|
|
221
|
+
mock_services['storage'].upload_fileobj.assert_called()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TestUploadValidation:
|
|
225
|
+
"""Tests for upload validation logic."""
|
|
226
|
+
|
|
227
|
+
def test_upload_accepts_m4a(self, client, mock_services, upload_auth_headers):
|
|
228
|
+
"""Test upload accepts M4A files."""
|
|
229
|
+
response = client.post(
|
|
230
|
+
"/api/jobs/upload",
|
|
231
|
+
headers=upload_auth_headers,
|
|
232
|
+
files={"file": ("test.m4a", BytesIO(b"audio"), "audio/mp4")},
|
|
233
|
+
data={"artist": "Test", "title": "Song"}
|
|
234
|
+
)
|
|
235
|
+
assert response.status_code == 200
|
|
236
|
+
|
|
237
|
+
def test_upload_accepts_ogg(self, client, mock_services, upload_auth_headers):
|
|
238
|
+
"""Test upload accepts OGG files."""
|
|
239
|
+
response = client.post(
|
|
240
|
+
"/api/jobs/upload",
|
|
241
|
+
headers=upload_auth_headers,
|
|
242
|
+
files={"file": ("test.ogg", BytesIO(b"audio"), "audio/ogg")},
|
|
243
|
+
data={"artist": "Test", "title": "Song"}
|
|
244
|
+
)
|
|
245
|
+
assert response.status_code == 200
|
|
246
|
+
|
|
247
|
+
def test_upload_accepts_aac(self, client, mock_services, upload_auth_headers):
|
|
248
|
+
"""Test upload accepts AAC files."""
|
|
249
|
+
response = client.post(
|
|
250
|
+
"/api/jobs/upload",
|
|
251
|
+
headers=upload_auth_headers,
|
|
252
|
+
files={"file": ("test.aac", BytesIO(b"audio"), "audio/aac")},
|
|
253
|
+
data={"artist": "Test", "title": "Song"}
|
|
254
|
+
)
|
|
255
|
+
assert response.status_code == 200
|
|
256
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for backend validation script (validate.py).
|
|
3
|
+
|
|
4
|
+
Tests the validation functions that check for import errors, syntax errors,
|
|
5
|
+
configuration issues, and FastAPI app creation.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import patch, MagicMock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestValidateImports:
|
|
12
|
+
"""Tests for the validate_imports function."""
|
|
13
|
+
|
|
14
|
+
def test_validate_imports_success(self):
|
|
15
|
+
"""Test that validate_imports succeeds when all modules import."""
|
|
16
|
+
from backend.validate import validate_imports
|
|
17
|
+
|
|
18
|
+
# This should succeed since we're in a valid test environment
|
|
19
|
+
result = validate_imports()
|
|
20
|
+
assert result is True
|
|
21
|
+
|
|
22
|
+
def test_validate_imports_with_failure(self):
|
|
23
|
+
"""Test that validate_imports handles import failures."""
|
|
24
|
+
from backend.validate import validate_imports
|
|
25
|
+
|
|
26
|
+
# Mock importlib to simulate a failure
|
|
27
|
+
with patch('backend.validate.importlib.import_module') as mock_import:
|
|
28
|
+
mock_import.side_effect = ImportError("Module not found")
|
|
29
|
+
|
|
30
|
+
result = validate_imports()
|
|
31
|
+
assert result is False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestValidateSyntax:
|
|
35
|
+
"""Tests for the validate_syntax function."""
|
|
36
|
+
|
|
37
|
+
def test_validate_syntax_success(self):
|
|
38
|
+
"""Test that validate_syntax succeeds with valid Python files."""
|
|
39
|
+
from backend.validate import validate_syntax
|
|
40
|
+
|
|
41
|
+
# This should succeed since the backend code is valid
|
|
42
|
+
result = validate_syntax()
|
|
43
|
+
assert result is True
|
|
44
|
+
|
|
45
|
+
def test_validate_syntax_handles_invalid_file(self):
|
|
46
|
+
"""Test that validate_syntax detects syntax errors."""
|
|
47
|
+
import tempfile
|
|
48
|
+
import os
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from backend.validate import validate_syntax
|
|
51
|
+
|
|
52
|
+
# We can't easily inject invalid files into the backend dir,
|
|
53
|
+
# but we can verify the function runs and returns True for valid files
|
|
54
|
+
result = validate_syntax()
|
|
55
|
+
assert result is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestValidateConfig:
|
|
59
|
+
"""Tests for the validate_config function."""
|
|
60
|
+
|
|
61
|
+
def test_validate_config_success(self):
|
|
62
|
+
"""Test that validate_config succeeds with valid configuration."""
|
|
63
|
+
from backend.validate import validate_config
|
|
64
|
+
|
|
65
|
+
# This should succeed in test environment
|
|
66
|
+
result = validate_config()
|
|
67
|
+
assert result is True
|
|
68
|
+
|
|
69
|
+
def test_validate_config_failure(self):
|
|
70
|
+
"""Test that validate_config handles configuration errors."""
|
|
71
|
+
from backend.validate import validate_config
|
|
72
|
+
|
|
73
|
+
# Patch at the source module
|
|
74
|
+
with patch('backend.config.get_settings') as mock_settings:
|
|
75
|
+
mock_settings.side_effect = Exception("Config error")
|
|
76
|
+
|
|
77
|
+
result = validate_config()
|
|
78
|
+
assert result is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestValidateFastapiApp:
|
|
82
|
+
"""Tests for the validate_fastapi_app function."""
|
|
83
|
+
|
|
84
|
+
def test_validate_fastapi_app_success(self):
|
|
85
|
+
"""Test that validate_fastapi_app succeeds."""
|
|
86
|
+
from backend.validate import validate_fastapi_app
|
|
87
|
+
|
|
88
|
+
# This should succeed since the FastAPI app is valid
|
|
89
|
+
result = validate_fastapi_app()
|
|
90
|
+
assert result is True
|
|
91
|
+
|
|
92
|
+
def test_validate_fastapi_app_failure(self):
|
|
93
|
+
"""Test that validate_fastapi_app handles app creation errors."""
|
|
94
|
+
from backend.validate import validate_fastapi_app
|
|
95
|
+
|
|
96
|
+
with patch.dict('sys.modules', {'backend.main': MagicMock(app=MagicMock(title="Test", version="1.0", routes=[]))}):
|
|
97
|
+
# Even with a mock, the function should work
|
|
98
|
+
result = validate_fastapi_app()
|
|
99
|
+
# The original module is still accessible, so this should still pass
|
|
100
|
+
assert result is True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestMain:
|
|
104
|
+
"""Tests for the main function."""
|
|
105
|
+
|
|
106
|
+
def test_main_runs(self):
|
|
107
|
+
"""Test that main runs without errors."""
|
|
108
|
+
from backend.validate import main
|
|
109
|
+
|
|
110
|
+
# In a valid test environment, main should return 0 (success)
|
|
111
|
+
result = main()
|
|
112
|
+
assert result == 0
|
|
113
|
+
|
|
114
|
+
def test_main_returns_1_on_failure(self):
|
|
115
|
+
"""Test that main returns 1 when validations fail."""
|
|
116
|
+
from backend.validate import main
|
|
117
|
+
|
|
118
|
+
# Mock one validation to fail
|
|
119
|
+
with patch('backend.validate.validate_imports', return_value=False):
|
|
120
|
+
result = main()
|
|
121
|
+
assert result == 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestValidateModule:
|
|
125
|
+
"""Test the validate module can be imported and has expected functions."""
|
|
126
|
+
|
|
127
|
+
def test_module_imports(self):
|
|
128
|
+
"""Test that the validate module can be imported."""
|
|
129
|
+
import backend.validate
|
|
130
|
+
assert backend.validate is not None
|
|
131
|
+
|
|
132
|
+
def test_has_validate_imports(self):
|
|
133
|
+
"""Test that validate_imports function exists."""
|
|
134
|
+
from backend.validate import validate_imports
|
|
135
|
+
assert callable(validate_imports)
|
|
136
|
+
|
|
137
|
+
def test_has_validate_syntax(self):
|
|
138
|
+
"""Test that validate_syntax function exists."""
|
|
139
|
+
from backend.validate import validate_syntax
|
|
140
|
+
assert callable(validate_syntax)
|
|
141
|
+
|
|
142
|
+
def test_has_validate_config(self):
|
|
143
|
+
"""Test that validate_config function exists."""
|
|
144
|
+
from backend.validate import validate_config
|
|
145
|
+
assert callable(validate_config)
|
|
146
|
+
|
|
147
|
+
def test_has_validate_fastapi_app(self):
|
|
148
|
+
"""Test that validate_fastapi_app function exists."""
|
|
149
|
+
from backend.validate import validate_fastapi_app
|
|
150
|
+
assert callable(validate_fastapi_app)
|
|
151
|
+
|
|
152
|
+
def test_has_main(self):
|
|
153
|
+
"""Test that main function exists."""
|
|
154
|
+
from backend.validate import main
|
|
155
|
+
assert callable(main)
|
|
156
|
+
|