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,443 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for job notification service.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import Mock, patch, AsyncMock
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
from backend.services.job_notification_service import (
|
|
9
|
+
JobNotificationService,
|
|
10
|
+
get_job_notification_service,
|
|
11
|
+
ENABLE_AUTO_EMAILS,
|
|
12
|
+
FEEDBACK_FORM_URL,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestURLBuilding:
|
|
17
|
+
"""Tests for URL building methods."""
|
|
18
|
+
|
|
19
|
+
def test_build_review_url_basic(self):
|
|
20
|
+
"""Test basic review URL building."""
|
|
21
|
+
service = JobNotificationService()
|
|
22
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
23
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
24
|
+
|
|
25
|
+
url = service._build_review_url("job-123")
|
|
26
|
+
|
|
27
|
+
assert "gen.nomadkaraoke.com/lyrics/" in url
|
|
28
|
+
assert "baseApiUrl=" in url
|
|
29
|
+
# The API URL should be URL-encoded
|
|
30
|
+
assert urllib.parse.quote("https://api.nomadkaraoke.com/api/review/job-123", safe='') in url
|
|
31
|
+
|
|
32
|
+
def test_build_review_url_with_audio_hash(self):
|
|
33
|
+
"""Test review URL with audio hash parameter."""
|
|
34
|
+
service = JobNotificationService()
|
|
35
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
36
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
37
|
+
|
|
38
|
+
url = service._build_review_url("job-123", audio_hash="abc123")
|
|
39
|
+
|
|
40
|
+
assert "audioHash=abc123" in url
|
|
41
|
+
|
|
42
|
+
def test_build_review_url_with_review_token(self):
|
|
43
|
+
"""Test review URL with review token parameter."""
|
|
44
|
+
service = JobNotificationService()
|
|
45
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
46
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
47
|
+
|
|
48
|
+
url = service._build_review_url("job-123", review_token="token456")
|
|
49
|
+
|
|
50
|
+
assert "reviewToken=token456" in url
|
|
51
|
+
|
|
52
|
+
def test_build_review_url_with_all_params(self):
|
|
53
|
+
"""Test review URL with all parameters."""
|
|
54
|
+
service = JobNotificationService()
|
|
55
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
56
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
57
|
+
|
|
58
|
+
url = service._build_review_url(
|
|
59
|
+
"job-123",
|
|
60
|
+
audio_hash="hash789",
|
|
61
|
+
review_token="token456"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert "baseApiUrl=" in url
|
|
65
|
+
assert "audioHash=hash789" in url
|
|
66
|
+
assert "reviewToken=token456" in url
|
|
67
|
+
|
|
68
|
+
def test_build_review_url_encodes_special_chars(self):
|
|
69
|
+
"""Test that special characters in job ID are encoded."""
|
|
70
|
+
service = JobNotificationService()
|
|
71
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
72
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
73
|
+
|
|
74
|
+
url = service._build_review_url("job/with/slashes")
|
|
75
|
+
|
|
76
|
+
# The baseApiUrl parameter should have encoded slashes
|
|
77
|
+
assert "%2F" in url
|
|
78
|
+
|
|
79
|
+
def test_build_instrumental_url_basic(self):
|
|
80
|
+
"""Test basic instrumental URL building."""
|
|
81
|
+
service = JobNotificationService()
|
|
82
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
83
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
84
|
+
|
|
85
|
+
url = service._build_instrumental_url("job-123")
|
|
86
|
+
|
|
87
|
+
assert "gen.nomadkaraoke.com/instrumental/" in url
|
|
88
|
+
assert "baseApiUrl=" in url
|
|
89
|
+
|
|
90
|
+
def test_build_instrumental_url_with_token(self):
|
|
91
|
+
"""Test instrumental URL with token parameter."""
|
|
92
|
+
service = JobNotificationService()
|
|
93
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
94
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
95
|
+
|
|
96
|
+
url = service._build_instrumental_url("job-123", instrumental_token="inst-token")
|
|
97
|
+
|
|
98
|
+
assert "instrumentalToken=inst-token" in url
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestCompletionEmail:
|
|
102
|
+
"""Tests for job completion email sending."""
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_send_completion_email_success(self):
|
|
106
|
+
"""Test successful completion email sending."""
|
|
107
|
+
service = JobNotificationService()
|
|
108
|
+
service.email_service = Mock()
|
|
109
|
+
service.email_service.send_job_completion.return_value = True
|
|
110
|
+
service.template_service = Mock()
|
|
111
|
+
service.template_service.render_job_completion.return_value = "Test message"
|
|
112
|
+
|
|
113
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
114
|
+
result = await service.send_job_completion_email(
|
|
115
|
+
job_id="job-123",
|
|
116
|
+
user_email="user@example.com",
|
|
117
|
+
user_name="Test User",
|
|
118
|
+
artist="Test Artist",
|
|
119
|
+
title="Test Song",
|
|
120
|
+
youtube_url="https://youtube.com/watch?v=123",
|
|
121
|
+
dropbox_url="https://dropbox.com/folder/abc",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert result is True
|
|
125
|
+
service.template_service.render_job_completion.assert_called_once()
|
|
126
|
+
service.email_service.send_job_completion.assert_called_once()
|
|
127
|
+
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_send_completion_email_disabled(self):
|
|
130
|
+
"""Test that completion email is skipped when auto emails are disabled."""
|
|
131
|
+
service = JobNotificationService()
|
|
132
|
+
service.email_service = Mock()
|
|
133
|
+
service.template_service = Mock()
|
|
134
|
+
|
|
135
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', False):
|
|
136
|
+
result = await service.send_job_completion_email(
|
|
137
|
+
job_id="job-123",
|
|
138
|
+
user_email="user@example.com",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert result is False
|
|
142
|
+
service.email_service.send_job_completion.assert_not_called()
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_send_completion_email_no_email(self):
|
|
146
|
+
"""Test that completion email is skipped when no user email."""
|
|
147
|
+
service = JobNotificationService()
|
|
148
|
+
service.email_service = Mock()
|
|
149
|
+
service.template_service = Mock()
|
|
150
|
+
|
|
151
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
152
|
+
result = await service.send_job_completion_email(
|
|
153
|
+
job_id="job-123",
|
|
154
|
+
user_email=None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert result is False
|
|
158
|
+
service.email_service.send_job_completion.assert_not_called()
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_send_completion_email_empty_email(self):
|
|
162
|
+
"""Test that completion email is skipped when user email is empty."""
|
|
163
|
+
service = JobNotificationService()
|
|
164
|
+
service.email_service = Mock()
|
|
165
|
+
service.template_service = Mock()
|
|
166
|
+
|
|
167
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
168
|
+
result = await service.send_job_completion_email(
|
|
169
|
+
job_id="job-123",
|
|
170
|
+
user_email="",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
assert result is False
|
|
174
|
+
service.email_service.send_job_completion.assert_not_called()
|
|
175
|
+
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_send_completion_email_send_failure(self):
|
|
178
|
+
"""Test handling of email send failure."""
|
|
179
|
+
service = JobNotificationService()
|
|
180
|
+
service.email_service = Mock()
|
|
181
|
+
service.email_service.send_job_completion.return_value = False
|
|
182
|
+
service.template_service = Mock()
|
|
183
|
+
service.template_service.render_job_completion.return_value = "Test message"
|
|
184
|
+
|
|
185
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
186
|
+
result = await service.send_job_completion_email(
|
|
187
|
+
job_id="job-123",
|
|
188
|
+
user_email="user@example.com",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert result is False
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_send_completion_email_exception(self):
|
|
195
|
+
"""Test handling of exceptions during email sending."""
|
|
196
|
+
service = JobNotificationService()
|
|
197
|
+
service.email_service = Mock()
|
|
198
|
+
service.email_service.send_job_completion.side_effect = Exception("Send error")
|
|
199
|
+
service.template_service = Mock()
|
|
200
|
+
service.template_service.render_job_completion.return_value = "Test message"
|
|
201
|
+
|
|
202
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
203
|
+
result = await service.send_job_completion_email(
|
|
204
|
+
job_id="job-123",
|
|
205
|
+
user_email="user@example.com",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
assert result is False
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_send_completion_email_passes_cc_admin(self):
|
|
212
|
+
"""Test that completion email is sent with CC to admin."""
|
|
213
|
+
service = JobNotificationService()
|
|
214
|
+
service.email_service = Mock()
|
|
215
|
+
service.email_service.send_job_completion.return_value = True
|
|
216
|
+
service.template_service = Mock()
|
|
217
|
+
service.template_service.render_job_completion.return_value = "Test message"
|
|
218
|
+
|
|
219
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
220
|
+
await service.send_job_completion_email(
|
|
221
|
+
job_id="job-123",
|
|
222
|
+
user_email="user@example.com",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Verify cc_admin=True was passed
|
|
226
|
+
call_kwargs = service.email_service.send_job_completion.call_args.kwargs
|
|
227
|
+
assert call_kwargs.get('cc_admin') is True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestActionReminderEmail:
|
|
231
|
+
"""Tests for action reminder email sending."""
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_send_lyrics_reminder_success(self):
|
|
235
|
+
"""Test successful lyrics reminder email sending."""
|
|
236
|
+
service = JobNotificationService()
|
|
237
|
+
service.email_service = Mock()
|
|
238
|
+
service.email_service.send_action_reminder.return_value = True
|
|
239
|
+
service.template_service = Mock()
|
|
240
|
+
service.template_service.render_action_needed_lyrics.return_value = "Review your lyrics"
|
|
241
|
+
|
|
242
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
243
|
+
result = await service.send_action_reminder_email(
|
|
244
|
+
job_id="job-123",
|
|
245
|
+
user_email="user@example.com",
|
|
246
|
+
action_type="lyrics",
|
|
247
|
+
artist="Test Artist",
|
|
248
|
+
title="Test Song",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert result is True
|
|
252
|
+
service.template_service.render_action_needed_lyrics.assert_called_once()
|
|
253
|
+
service.email_service.send_action_reminder.assert_called_once()
|
|
254
|
+
|
|
255
|
+
@pytest.mark.asyncio
|
|
256
|
+
async def test_send_instrumental_reminder_success(self):
|
|
257
|
+
"""Test successful instrumental reminder email sending."""
|
|
258
|
+
service = JobNotificationService()
|
|
259
|
+
service.email_service = Mock()
|
|
260
|
+
service.email_service.send_action_reminder.return_value = True
|
|
261
|
+
service.template_service = Mock()
|
|
262
|
+
service.template_service.render_action_needed_instrumental.return_value = "Select instrumental"
|
|
263
|
+
|
|
264
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
265
|
+
result = await service.send_action_reminder_email(
|
|
266
|
+
job_id="job-123",
|
|
267
|
+
user_email="user@example.com",
|
|
268
|
+
action_type="instrumental",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
assert result is True
|
|
272
|
+
service.template_service.render_action_needed_instrumental.assert_called_once()
|
|
273
|
+
service.email_service.send_action_reminder.assert_called_once()
|
|
274
|
+
|
|
275
|
+
@pytest.mark.asyncio
|
|
276
|
+
async def test_send_reminder_disabled(self):
|
|
277
|
+
"""Test that reminder email is skipped when auto emails are disabled."""
|
|
278
|
+
service = JobNotificationService()
|
|
279
|
+
service.email_service = Mock()
|
|
280
|
+
|
|
281
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', False):
|
|
282
|
+
result = await service.send_action_reminder_email(
|
|
283
|
+
job_id="job-123",
|
|
284
|
+
user_email="user@example.com",
|
|
285
|
+
action_type="lyrics",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
assert result is False
|
|
289
|
+
service.email_service.send_action_reminder.assert_not_called()
|
|
290
|
+
|
|
291
|
+
@pytest.mark.asyncio
|
|
292
|
+
async def test_send_reminder_no_email(self):
|
|
293
|
+
"""Test that reminder email is skipped when no user email."""
|
|
294
|
+
service = JobNotificationService()
|
|
295
|
+
service.email_service = Mock()
|
|
296
|
+
|
|
297
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
298
|
+
result = await service.send_action_reminder_email(
|
|
299
|
+
job_id="job-123",
|
|
300
|
+
user_email=None,
|
|
301
|
+
action_type="lyrics",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
assert result is False
|
|
305
|
+
|
|
306
|
+
@pytest.mark.asyncio
|
|
307
|
+
async def test_send_reminder_unknown_action_type(self):
|
|
308
|
+
"""Test handling of unknown action type."""
|
|
309
|
+
service = JobNotificationService()
|
|
310
|
+
service.email_service = Mock()
|
|
311
|
+
service.template_service = Mock()
|
|
312
|
+
|
|
313
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
314
|
+
result = await service.send_action_reminder_email(
|
|
315
|
+
job_id="job-123",
|
|
316
|
+
user_email="user@example.com",
|
|
317
|
+
action_type="unknown",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
assert result is False
|
|
321
|
+
service.email_service.send_action_reminder.assert_not_called()
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
async def test_send_lyrics_reminder_includes_review_url(self):
|
|
325
|
+
"""Test that lyrics reminder includes correct review URL."""
|
|
326
|
+
service = JobNotificationService()
|
|
327
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
328
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
329
|
+
service.email_service = Mock()
|
|
330
|
+
service.email_service.send_action_reminder.return_value = True
|
|
331
|
+
service.template_service = Mock()
|
|
332
|
+
service.template_service.render_action_needed_lyrics.return_value = "Review"
|
|
333
|
+
|
|
334
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
335
|
+
await service.send_action_reminder_email(
|
|
336
|
+
job_id="job-123",
|
|
337
|
+
user_email="user@example.com",
|
|
338
|
+
action_type="lyrics",
|
|
339
|
+
audio_hash="hash123",
|
|
340
|
+
review_token="token456",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Verify the review URL was passed to template
|
|
344
|
+
call_kwargs = service.template_service.render_action_needed_lyrics.call_args.kwargs
|
|
345
|
+
review_url = call_kwargs.get('review_url')
|
|
346
|
+
assert "audioHash=hash123" in review_url
|
|
347
|
+
assert "reviewToken=token456" in review_url
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_send_instrumental_reminder_includes_url(self):
|
|
351
|
+
"""Test that instrumental reminder includes correct URL."""
|
|
352
|
+
service = JobNotificationService()
|
|
353
|
+
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
354
|
+
service.backend_url = "https://api.nomadkaraoke.com"
|
|
355
|
+
service.email_service = Mock()
|
|
356
|
+
service.email_service.send_action_reminder.return_value = True
|
|
357
|
+
service.template_service = Mock()
|
|
358
|
+
service.template_service.render_action_needed_instrumental.return_value = "Select"
|
|
359
|
+
|
|
360
|
+
with patch('backend.services.job_notification_service.ENABLE_AUTO_EMAILS', True):
|
|
361
|
+
await service.send_action_reminder_email(
|
|
362
|
+
job_id="job-123",
|
|
363
|
+
user_email="user@example.com",
|
|
364
|
+
action_type="instrumental",
|
|
365
|
+
instrumental_token="inst-token",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Verify the instrumental URL was passed to template
|
|
369
|
+
call_kwargs = service.template_service.render_action_needed_instrumental.call_args.kwargs
|
|
370
|
+
instrumental_url = call_kwargs.get('instrumental_url')
|
|
371
|
+
assert "instrumentalToken=inst-token" in instrumental_url
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestGetCompletionMessage:
|
|
375
|
+
"""Tests for get_completion_message method."""
|
|
376
|
+
|
|
377
|
+
def test_get_completion_message_basic(self):
|
|
378
|
+
"""Test basic completion message retrieval."""
|
|
379
|
+
service = JobNotificationService()
|
|
380
|
+
service.template_service = Mock()
|
|
381
|
+
service.template_service.render_job_completion.return_value = "Your video is ready!"
|
|
382
|
+
|
|
383
|
+
result = service.get_completion_message(
|
|
384
|
+
job_id="job-123",
|
|
385
|
+
user_name="Test User",
|
|
386
|
+
artist="Test Artist",
|
|
387
|
+
title="Test Song",
|
|
388
|
+
youtube_url="https://youtube.com/watch?v=123",
|
|
389
|
+
dropbox_url="https://dropbox.com/folder/abc",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
assert result == "Your video is ready!"
|
|
393
|
+
service.template_service.render_job_completion.assert_called_once()
|
|
394
|
+
|
|
395
|
+
def test_get_completion_message_includes_feedback_url(self):
|
|
396
|
+
"""Test that completion message includes feedback URL."""
|
|
397
|
+
service = JobNotificationService()
|
|
398
|
+
service.template_service = Mock()
|
|
399
|
+
service.template_service.render_job_completion.return_value = "Message"
|
|
400
|
+
|
|
401
|
+
service.get_completion_message(job_id="job-123")
|
|
402
|
+
|
|
403
|
+
# Verify feedback URL was passed
|
|
404
|
+
call_kwargs = service.template_service.render_job_completion.call_args.kwargs
|
|
405
|
+
assert 'feedback_url' in call_kwargs
|
|
406
|
+
|
|
407
|
+
def test_get_completion_message_with_defaults(self):
|
|
408
|
+
"""Test completion message with default values."""
|
|
409
|
+
service = JobNotificationService()
|
|
410
|
+
service.template_service = Mock()
|
|
411
|
+
service.template_service.render_job_completion.return_value = "Message"
|
|
412
|
+
|
|
413
|
+
service.get_completion_message(job_id="job-123")
|
|
414
|
+
|
|
415
|
+
call_kwargs = service.template_service.render_job_completion.call_args.kwargs
|
|
416
|
+
assert call_kwargs.get('job_id') == "job-123"
|
|
417
|
+
# Other params should be None/default
|
|
418
|
+
assert call_kwargs.get('name') is None
|
|
419
|
+
assert call_kwargs.get('artist') is None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class TestGlobalInstance:
|
|
423
|
+
"""Tests for global instance management."""
|
|
424
|
+
|
|
425
|
+
def test_get_job_notification_service_returns_same_instance(self):
|
|
426
|
+
"""Test that get_job_notification_service returns singleton."""
|
|
427
|
+
# Reset global
|
|
428
|
+
import backend.services.job_notification_service as jns
|
|
429
|
+
jns._job_notification_service = None
|
|
430
|
+
|
|
431
|
+
service1 = get_job_notification_service()
|
|
432
|
+
service2 = get_job_notification_service()
|
|
433
|
+
|
|
434
|
+
assert service1 is service2
|
|
435
|
+
|
|
436
|
+
def test_service_initializes_with_dependencies(self):
|
|
437
|
+
"""Test that service initializes with email and template services."""
|
|
438
|
+
service = JobNotificationService()
|
|
439
|
+
|
|
440
|
+
assert service.email_service is not None
|
|
441
|
+
assert service.template_service is not None
|
|
442
|
+
assert service.frontend_url is not None
|
|
443
|
+
assert service.backend_url is not None
|