karaoke-gen 0.103.1__py3-none-any.whl → 0.107.0__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/Dockerfile.base +1 -0
- backend/api/routes/admin.py +226 -3
- backend/api/routes/push.py +238 -0
- backend/api/routes/users.py +14 -3
- backend/config.py +12 -1
- backend/main.py +2 -1
- backend/models/job.py +4 -0
- backend/models/user.py +20 -2
- backend/services/encoding_interface.py +4 -0
- backend/services/gce_encoding/main.py +22 -8
- backend/services/job_manager.py +68 -11
- backend/services/job_notification_service.py +4 -21
- backend/services/push_notification_service.py +409 -0
- backend/services/stripe_service.py +2 -2
- backend/tests/conftest.py +2 -1
- backend/tests/test_admin_delete_outputs.py +352 -0
- backend/tests/test_gce_encoding_worker.py +229 -0
- backend/tests/test_impersonation.py +18 -3
- backend/tests/test_job_notification_service.py +24 -58
- backend/tests/test_push_notification_service.py +460 -0
- backend/tests/test_push_routes.py +357 -0
- backend/tests/test_stripe_service.py +205 -0
- backend/tests/test_video_worker_orchestrator.py +189 -0
- backend/workers/video_worker_orchestrator.py +23 -0
- karaoke_gen/instrumental_review/server.py +145 -35
- karaoke_gen/nextjs_frontend/__init__.py +98 -0
- karaoke_gen/nextjs_frontend/out/404/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/404.html +1 -0
- karaoke_gen/nextjs_frontend/out/__next.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/01a7f8fe40f1ff47.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/112f346e31f991df.js +4 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/16d1a4dd9d8a873a.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/1ab85c362b8b0e86.js +9 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/247eb132b7f7b574.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/2b80d15cc95e4818.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/32c7eba5cd46c1bc.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/483f26794eae53d0.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/550c3b02e85f196a.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/55c5ade44387bef8.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5628d92b5893add2.css +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/56ebf7665e4341c8.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5997132b61dec430.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5ea55255bce3eb9e.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/5eda89a57490b3cd.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/692f5d9e0d700c76.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/71d7a05b14f9f0f4.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/81ac355749ef3302.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/95f7e5934dbb0e5d.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/9bce8f19eaa46940.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/a9ed54eed3e14c92.js +2 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b35cd41238ecfb17.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5bc3c3d5ebd49eb.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/b5c078c08db5ae32.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/be9c44a178104187.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/c4c840e18cb4861c.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/c645af7d6b65f73e.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d2c5e2575df784d4.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d30af02b96d81462.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/d9bdf64f4ec1e9b7.js +7 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/dcde6ed684dacd0e.js +5 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e422cbe931246000.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e483af34fc792d38.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/e57422aad6b897da.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/ef02697fb404726a.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/ff1a16fafef87110.js +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/chunks/turbopack-2d9ca3017a9deedf.js +3 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_buildManifest.js +11 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_clientMiddlewareManifest.json +1 -0
- karaoke_gen/nextjs_frontend/out/_next/static/zpw_-rjFIDV5tlPPtnvRI/_ssgManifest.js +1 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._full.txt +18 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._not-found.txt +4 -0
- karaoke_gen/nextjs_frontend/out/_not-found/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/_not-found/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/_not-found/index.txt +18 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/__next.admin.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.beta.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/beta/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/jobs/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.rate-limits.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/rate-limits/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.searches.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/searches/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/__next.admin.users.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._full.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.txt +7 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.detail.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/__next.admin.users.txt +4 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/users/detail/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/admin/users/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/admin/users/index.txt +25 -0
- karaoke_gen/nextjs_frontend/out/app/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/app/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/__next.app.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/instrumental/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._full.txt +19 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.__PAGE__.txt +6 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.$oc$slug.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.jobs.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/__next.app.txt +4 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/app/jobs/local/review/index.txt +19 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.txt +4 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/__next.auth.verify.txt +4 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/auth/verify/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/manifest.webmanifest +31 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.success.txt +4 -0
- karaoke_gen/nextjs_frontend/out/order/success/__next.order.txt +4 -0
- karaoke_gen/nextjs_frontend/out/order/success/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/order/success/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._full.txt +22 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._head.txt +8 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._index.txt +9 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next._tree.txt +2 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.__PAGE__.txt +9 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.success.txt +4 -0
- karaoke_gen/nextjs_frontend/out/payment/success/__next.payment.txt +4 -0
- karaoke_gen/nextjs_frontend/out/payment/success/index.html +1 -0
- karaoke_gen/nextjs_frontend/out/payment/success/index.txt +22 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-action_reminder.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-beta_welcome.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/email-job_completion.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/example-output.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/homepage-full.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/homepage-hero.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/instrumental-review.png +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/job-dashboard.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.avif +0 -0
- karaoke_gen/nextjs_frontend/out/screenshots/lyrics-review.png +0 -0
- karaoke_gen/nextjs_frontend/out/sw.js +183 -0
- karaoke_gen/utils/cli_args.py +3 -3
- karaoke_gen/utils/gen_cli.py +4 -0
- karaoke_gen/utils/remote_cli.py +8 -40
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/METADATA +2 -1
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/RECORD +244 -131
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/correction/agentic/agent.py +83 -60
- lyrics_transcriber/correction/anchor_sequence.py +48 -3
- lyrics_transcriber/correction/corrector.py +92 -58
- lyrics_transcriber/review/server.py +165 -33
- lyrics_transcriber/utils/tracing.py +214 -0
- karaoke_gen/instrumental_review/static/index.html +0 -1695
- lyrics_transcriber/frontend/.gitignore +0 -24
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +0 -935
- lyrics_transcriber/frontend/.yarnrc.yml +0 -3
- lyrics_transcriber/frontend/README.md +0 -50
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +0 -210
- lyrics_transcriber/frontend/__init__.py +0 -25
- lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +0 -207
- lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +0 -226
- lyrics_transcriber/frontend/eslint.config.js +0 -28
- lyrics_transcriber/frontend/index.html +0 -22
- lyrics_transcriber/frontend/package-lock.json +0 -4553
- lyrics_transcriber/frontend/package.json +0 -48
- lyrics_transcriber/frontend/playwright.config.ts +0 -69
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +0 -243
- lyrics_transcriber/frontend/src/api.ts +0 -262
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +0 -111
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +0 -114
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +0 -204
- lyrics_transcriber/frontend/src/components/AppHeader.tsx +0 -65
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +0 -180
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +0 -175
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +0 -359
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +0 -281
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +0 -162
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +0 -257
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +0 -94
- lyrics_transcriber/frontend/src/components/EditModal.tsx +0 -720
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +0 -592
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +0 -431
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +0 -77
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +0 -467
- lyrics_transcriber/frontend/src/components/Header.tsx +0 -520
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +0 -1526
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +0 -216
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +0 -721
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +0 -80
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +0 -999
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +0 -51
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +0 -127
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +0 -67
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +0 -23
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +0 -177
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +0 -268
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +0 -336
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +0 -354
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +0 -64
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +0 -383
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +0 -131
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -266
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +0 -191
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -466
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +0 -56
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +0 -89
- lyrics_transcriber/frontend/src/components/shared/constants.ts +0 -30
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +0 -180
- lyrics_transcriber/frontend/src/components/shared/styles.ts +0 -13
- lyrics_transcriber/frontend/src/components/shared/types.js +0 -2
- lyrics_transcriber/frontend/src/components/shared/types.ts +0 -135
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +0 -177
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +0 -78
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +0 -75
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +0 -360
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +0 -110
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +0 -22
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +0 -537
- lyrics_transcriber/frontend/src/main.tsx +0 -11
- lyrics_transcriber/frontend/src/theme.ts +0 -406
- lyrics_transcriber/frontend/src/types/global.d.ts +0 -9
- lyrics_transcriber/frontend/src/types.js +0 -2
- lyrics_transcriber/frontend/src/types.ts +0 -199
- lyrics_transcriber/frontend/src/validation.ts +0 -132
- lyrics_transcriber/frontend/src/vite-env.d.ts +0 -1
- lyrics_transcriber/frontend/tsconfig.app.json +0 -26
- lyrics_transcriber/frontend/tsconfig.json +0 -25
- lyrics_transcriber/frontend/tsconfig.node.json +0 -23
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +0 -1
- lyrics_transcriber/frontend/update_version.js +0 -11
- lyrics_transcriber/frontend/vite.config.d.ts +0 -2
- lyrics_transcriber/frontend/vite.config.js +0 -15
- lyrics_transcriber/frontend/vite.config.ts +0 -16
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js +0 -44465
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +0 -1
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +0 -22
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +0 -5
- lyrics_transcriber/frontend/yarn.lock +0 -3711
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/apple-touch-icon.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-16x16.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon-32x32.png +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/favicon.ico +0 -0
- {lyrics_transcriber/frontend/public → karaoke_gen/nextjs_frontend/out}/nomad-karaoke-logo.svg +0 -0
- /lyrics_transcriber/frontend/public/nomad-karaoke-logo.png → /karaoke_gen/nextjs_frontend/out/nomad-logo.png +0 -0
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.103.1.dist-info → karaoke_gen-0.107.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,7 +3,6 @@ Unit tests for job notification service.
|
|
|
3
3
|
"""
|
|
4
4
|
import pytest
|
|
5
5
|
from unittest.mock import Mock, patch, AsyncMock
|
|
6
|
-
import urllib.parse
|
|
7
6
|
|
|
8
7
|
from backend.services.job_notification_service import (
|
|
9
8
|
JobNotificationService,
|
|
@@ -20,82 +19,55 @@ class TestURLBuilding:
|
|
|
20
19
|
"""Test basic review URL building."""
|
|
21
20
|
service = JobNotificationService()
|
|
22
21
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
23
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
24
22
|
|
|
25
23
|
url = service._build_review_url("job-123")
|
|
26
24
|
|
|
27
|
-
assert "gen.nomadkaraoke.com/
|
|
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
|
|
25
|
+
assert url == "https://gen.nomadkaraoke.com/app/jobs/job-123/review"
|
|
31
26
|
|
|
32
|
-
def
|
|
33
|
-
"""Test review URL
|
|
27
|
+
def test_build_review_url_ignores_legacy_params(self):
|
|
28
|
+
"""Test review URL ignores legacy audio_hash and review_token params."""
|
|
34
29
|
service = JobNotificationService()
|
|
35
30
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
36
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
# Legacy params are still accepted but not used in the URL
|
|
33
|
+
url = service._build_review_url("job-123", audio_hash="abc123", review_token="token456")
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
# URL should be the simple consolidated route
|
|
36
|
+
assert url == "https://gen.nomadkaraoke.com/app/jobs/job-123/review"
|
|
37
|
+
# No query params
|
|
38
|
+
assert "?" not in url
|
|
41
39
|
|
|
42
|
-
def
|
|
43
|
-
"""Test
|
|
40
|
+
def test_build_review_url_preserves_job_id_characters(self):
|
|
41
|
+
"""Test that job ID is used directly in URL path."""
|
|
44
42
|
service = JobNotificationService()
|
|
45
43
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
46
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
# Note: job IDs with slashes would be unusual in real usage
|
|
46
|
+
url = service._build_review_url("abc-def-123")
|
|
49
47
|
|
|
50
|
-
assert "
|
|
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
|
|
48
|
+
assert url == "https://gen.nomadkaraoke.com/app/jobs/abc-def-123/review"
|
|
78
49
|
|
|
79
50
|
def test_build_instrumental_url_basic(self):
|
|
80
51
|
"""Test basic instrumental URL building."""
|
|
81
52
|
service = JobNotificationService()
|
|
82
53
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
83
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
84
54
|
|
|
85
55
|
url = service._build_instrumental_url("job-123")
|
|
86
56
|
|
|
87
|
-
assert "gen.nomadkaraoke.com/instrumental
|
|
88
|
-
assert "baseApiUrl=" in url
|
|
57
|
+
assert url == "https://gen.nomadkaraoke.com/app/jobs/job-123/instrumental"
|
|
89
58
|
|
|
90
|
-
def
|
|
91
|
-
"""Test instrumental URL
|
|
59
|
+
def test_build_instrumental_url_ignores_legacy_params(self):
|
|
60
|
+
"""Test instrumental URL ignores legacy instrumental_token param."""
|
|
92
61
|
service = JobNotificationService()
|
|
93
62
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
94
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
95
63
|
|
|
64
|
+
# Legacy param is still accepted but not used in the URL
|
|
96
65
|
url = service._build_instrumental_url("job-123", instrumental_token="inst-token")
|
|
97
66
|
|
|
98
|
-
|
|
67
|
+
# URL should be the simple consolidated route
|
|
68
|
+
assert url == "https://gen.nomadkaraoke.com/app/jobs/job-123/instrumental"
|
|
69
|
+
# No query params
|
|
70
|
+
assert "?" not in url
|
|
99
71
|
|
|
100
72
|
|
|
101
73
|
class TestCompletionEmail:
|
|
@@ -325,7 +297,6 @@ class TestActionReminderEmail:
|
|
|
325
297
|
"""Test that lyrics reminder includes correct review URL."""
|
|
326
298
|
service = JobNotificationService()
|
|
327
299
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
328
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
329
300
|
service.email_service = Mock()
|
|
330
301
|
service.email_service.send_action_reminder.return_value = True
|
|
331
302
|
service.template_service = Mock()
|
|
@@ -336,22 +307,18 @@ class TestActionReminderEmail:
|
|
|
336
307
|
job_id="job-123",
|
|
337
308
|
user_email="user@example.com",
|
|
338
309
|
action_type="lyrics",
|
|
339
|
-
audio_hash="hash123",
|
|
340
|
-
review_token="token456",
|
|
341
310
|
)
|
|
342
311
|
|
|
343
312
|
# Verify the review URL was passed to template
|
|
344
313
|
call_kwargs = service.template_service.render_action_needed_lyrics.call_args.kwargs
|
|
345
314
|
review_url = call_kwargs.get('review_url')
|
|
346
|
-
assert "
|
|
347
|
-
assert "reviewToken=token456" in review_url
|
|
315
|
+
assert review_url == "https://gen.nomadkaraoke.com/app/jobs/job-123/review"
|
|
348
316
|
|
|
349
317
|
@pytest.mark.asyncio
|
|
350
318
|
async def test_send_instrumental_reminder_includes_url(self):
|
|
351
319
|
"""Test that instrumental reminder includes correct URL."""
|
|
352
320
|
service = JobNotificationService()
|
|
353
321
|
service.frontend_url = "https://gen.nomadkaraoke.com"
|
|
354
|
-
service.backend_url = "https://api.nomadkaraoke.com"
|
|
355
322
|
service.email_service = Mock()
|
|
356
323
|
service.email_service.send_action_reminder.return_value = True
|
|
357
324
|
service.template_service = Mock()
|
|
@@ -362,13 +329,12 @@ class TestActionReminderEmail:
|
|
|
362
329
|
job_id="job-123",
|
|
363
330
|
user_email="user@example.com",
|
|
364
331
|
action_type="instrumental",
|
|
365
|
-
instrumental_token="inst-token",
|
|
366
332
|
)
|
|
367
333
|
|
|
368
334
|
# Verify the instrumental URL was passed to template
|
|
369
335
|
call_kwargs = service.template_service.render_action_needed_instrumental.call_args.kwargs
|
|
370
336
|
instrumental_url = call_kwargs.get('instrumental_url')
|
|
371
|
-
assert "
|
|
337
|
+
assert instrumental_url == "https://gen.nomadkaraoke.com/app/jobs/job-123/instrumental"
|
|
372
338
|
|
|
373
339
|
|
|
374
340
|
class TestGetCompletionMessage:
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for PushNotificationService.
|
|
3
|
+
|
|
4
|
+
Tests the Web Push notification sending and subscription management.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from backend.services.push_notification_service import (
|
|
11
|
+
PushNotificationService,
|
|
12
|
+
get_push_notification_service,
|
|
13
|
+
SubscriptionGoneError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_settings():
|
|
19
|
+
"""Mock settings with push notifications enabled."""
|
|
20
|
+
settings = Mock()
|
|
21
|
+
settings.enable_push_notifications = True
|
|
22
|
+
settings.max_push_subscriptions_per_user = 5
|
|
23
|
+
settings.vapid_subject = "mailto:test@example.com"
|
|
24
|
+
settings.get_secret = Mock(side_effect=lambda x: {
|
|
25
|
+
"vapid-public-key": "test-public-key",
|
|
26
|
+
"vapid-private-key": "test-private-key"
|
|
27
|
+
}.get(x))
|
|
28
|
+
return settings
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_db():
|
|
33
|
+
"""Mock Firestore client."""
|
|
34
|
+
return Mock()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def push_service(mock_settings, mock_db):
|
|
39
|
+
"""Create PushNotificationService with mocked dependencies."""
|
|
40
|
+
with patch('backend.services.push_notification_service.get_settings', return_value=mock_settings):
|
|
41
|
+
service = PushNotificationService(db=mock_db)
|
|
42
|
+
return service
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestPushNotificationServiceInit:
|
|
46
|
+
"""Tests for service initialization and configuration."""
|
|
47
|
+
|
|
48
|
+
def test_is_enabled_when_configured(self, push_service):
|
|
49
|
+
"""Service reports enabled when all config present."""
|
|
50
|
+
assert push_service.is_enabled() is True
|
|
51
|
+
|
|
52
|
+
def test_is_disabled_when_feature_flag_off(self, mock_db):
|
|
53
|
+
"""Service reports disabled when feature flag off."""
|
|
54
|
+
settings = Mock()
|
|
55
|
+
settings.enable_push_notifications = False
|
|
56
|
+
settings.get_secret = Mock(return_value="key")
|
|
57
|
+
|
|
58
|
+
with patch('backend.services.push_notification_service.get_settings', return_value=settings):
|
|
59
|
+
service = PushNotificationService(db=mock_db)
|
|
60
|
+
assert service.is_enabled() is False
|
|
61
|
+
|
|
62
|
+
def test_is_disabled_when_vapid_keys_missing(self, mock_db):
|
|
63
|
+
"""Service reports disabled when VAPID keys missing."""
|
|
64
|
+
settings = Mock()
|
|
65
|
+
settings.enable_push_notifications = True
|
|
66
|
+
settings.get_secret = Mock(return_value=None)
|
|
67
|
+
|
|
68
|
+
with patch('backend.services.push_notification_service.get_settings', return_value=settings):
|
|
69
|
+
service = PushNotificationService(db=mock_db)
|
|
70
|
+
assert service.is_enabled() is False
|
|
71
|
+
|
|
72
|
+
def test_get_public_key(self, push_service):
|
|
73
|
+
"""Service returns public key when enabled."""
|
|
74
|
+
assert push_service.get_public_key() == "test-public-key"
|
|
75
|
+
|
|
76
|
+
def test_get_public_key_returns_none_when_disabled(self, mock_db):
|
|
77
|
+
"""Service returns None for public key when disabled."""
|
|
78
|
+
settings = Mock()
|
|
79
|
+
settings.enable_push_notifications = False
|
|
80
|
+
settings.get_secret = Mock(return_value="key")
|
|
81
|
+
|
|
82
|
+
with patch('backend.services.push_notification_service.get_settings', return_value=settings):
|
|
83
|
+
service = PushNotificationService(db=mock_db)
|
|
84
|
+
assert service.get_public_key() is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestSendPush:
|
|
88
|
+
"""Tests for sending push notifications."""
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_send_push_skips_when_disabled(self, mock_db):
|
|
92
|
+
"""send_push returns 0 when push notifications disabled."""
|
|
93
|
+
settings = Mock()
|
|
94
|
+
settings.enable_push_notifications = False
|
|
95
|
+
settings.get_secret = Mock(return_value=None)
|
|
96
|
+
|
|
97
|
+
with patch('backend.services.push_notification_service.get_settings', return_value=settings):
|
|
98
|
+
service = PushNotificationService(db=mock_db)
|
|
99
|
+
result = await service.send_push("test@example.com", "Title", "Body")
|
|
100
|
+
assert result == 0
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_send_push_no_user(self, push_service):
|
|
104
|
+
"""send_push returns 0 when user not found."""
|
|
105
|
+
# Mock user not existing
|
|
106
|
+
push_service.db.collection.return_value.document.return_value.get.return_value.exists = False
|
|
107
|
+
|
|
108
|
+
result = await push_service.send_push("unknown@example.com", "Title", "Body")
|
|
109
|
+
assert result == 0
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_send_push_no_subscriptions(self, push_service):
|
|
113
|
+
"""send_push returns 0 when user has no subscriptions."""
|
|
114
|
+
# Mock user with no subscriptions
|
|
115
|
+
mock_doc = Mock()
|
|
116
|
+
mock_doc.exists = True
|
|
117
|
+
mock_doc.to_dict.return_value = {"push_subscriptions": []}
|
|
118
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
119
|
+
|
|
120
|
+
result = await push_service.send_push("test@example.com", "Title", "Body")
|
|
121
|
+
assert result == 0
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_send_push_success(self, push_service):
|
|
125
|
+
"""send_push successfully sends to subscription."""
|
|
126
|
+
# Mock user with subscription
|
|
127
|
+
mock_doc = Mock()
|
|
128
|
+
mock_doc.exists = True
|
|
129
|
+
mock_doc.to_dict.return_value = {
|
|
130
|
+
"push_subscriptions": [{
|
|
131
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
132
|
+
"keys": {"p256dh": "key1", "auth": "key2"},
|
|
133
|
+
"device_name": "Test Device"
|
|
134
|
+
}]
|
|
135
|
+
}
|
|
136
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
137
|
+
|
|
138
|
+
with patch('backend.services.push_notification_service.webpush') as mock_webpush:
|
|
139
|
+
result = await push_service.send_push("test@example.com", "Title", "Body")
|
|
140
|
+
|
|
141
|
+
assert result == 1
|
|
142
|
+
mock_webpush.assert_called_once()
|
|
143
|
+
call_args = mock_webpush.call_args
|
|
144
|
+
assert call_args[1]["subscription_info"]["endpoint"] == "https://push.example.com/endpoint"
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_send_push_removes_gone_subscription(self, push_service):
|
|
148
|
+
"""send_push removes subscription when 410 Gone returned."""
|
|
149
|
+
from pywebpush import WebPushException
|
|
150
|
+
|
|
151
|
+
# Mock user with subscription
|
|
152
|
+
mock_doc = Mock()
|
|
153
|
+
mock_doc.exists = True
|
|
154
|
+
mock_doc.to_dict.return_value = {
|
|
155
|
+
"push_subscriptions": [{
|
|
156
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
157
|
+
"keys": {"p256dh": "key1", "auth": "key2"},
|
|
158
|
+
"device_name": "Test Device"
|
|
159
|
+
}]
|
|
160
|
+
}
|
|
161
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
162
|
+
|
|
163
|
+
# Mock webpush to raise 410 error
|
|
164
|
+
mock_response = Mock()
|
|
165
|
+
mock_response.status_code = 410
|
|
166
|
+
error = WebPushException("Gone", response=mock_response)
|
|
167
|
+
|
|
168
|
+
with patch('backend.services.push_notification_service.webpush', side_effect=error):
|
|
169
|
+
result = await push_service.send_push("test@example.com", "Title", "Body")
|
|
170
|
+
|
|
171
|
+
assert result == 0
|
|
172
|
+
# Verify invalid subscription was cleaned up
|
|
173
|
+
push_service.db.collection.return_value.document.return_value.update.assert_called()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestSubscriptionManagement:
|
|
177
|
+
"""Tests for adding, removing, and listing subscriptions."""
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_add_subscription_new_user(self, push_service):
|
|
181
|
+
"""add_subscription returns False for non-existent user."""
|
|
182
|
+
push_service.db.collection.return_value.document.return_value.get.return_value.exists = False
|
|
183
|
+
|
|
184
|
+
result = await push_service.add_subscription(
|
|
185
|
+
"unknown@example.com",
|
|
186
|
+
"https://push.example.com/endpoint",
|
|
187
|
+
{"p256dh": "key1", "auth": "key2"},
|
|
188
|
+
"Test Device"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert result is False
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_add_subscription_success(self, push_service):
|
|
195
|
+
"""add_subscription adds new subscription."""
|
|
196
|
+
mock_doc = Mock()
|
|
197
|
+
mock_doc.exists = True
|
|
198
|
+
mock_doc.to_dict.return_value = {"push_subscriptions": []}
|
|
199
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
200
|
+
|
|
201
|
+
result = await push_service.add_subscription(
|
|
202
|
+
"test@example.com",
|
|
203
|
+
"https://push.example.com/endpoint",
|
|
204
|
+
{"p256dh": "key1", "auth": "key2"},
|
|
205
|
+
"Test Device"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
assert result is True
|
|
209
|
+
push_service.db.collection.return_value.document.return_value.update.assert_called_once()
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_add_subscription_updates_existing(self, push_service):
|
|
213
|
+
"""add_subscription updates existing subscription with same endpoint."""
|
|
214
|
+
mock_doc = Mock()
|
|
215
|
+
mock_doc.exists = True
|
|
216
|
+
mock_doc.to_dict.return_value = {
|
|
217
|
+
"push_subscriptions": [{
|
|
218
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
219
|
+
"keys": {"p256dh": "old-key", "auth": "old-auth"},
|
|
220
|
+
"device_name": "Old Device"
|
|
221
|
+
}]
|
|
222
|
+
}
|
|
223
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
224
|
+
|
|
225
|
+
result = await push_service.add_subscription(
|
|
226
|
+
"test@example.com",
|
|
227
|
+
"https://push.example.com/endpoint",
|
|
228
|
+
{"p256dh": "new-key", "auth": "new-auth"},
|
|
229
|
+
"New Device"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
assert result is True
|
|
233
|
+
# Verify update was called (subscription replaced, not added)
|
|
234
|
+
update_call = push_service.db.collection.return_value.document.return_value.update.call_args
|
|
235
|
+
subs = update_call[0][0]["push_subscriptions"]
|
|
236
|
+
assert len(subs) == 1
|
|
237
|
+
assert subs[0]["device_name"] == "New Device"
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_add_subscription_enforces_max_limit(self, push_service):
|
|
241
|
+
"""add_subscription removes oldest when max exceeded."""
|
|
242
|
+
# Create 5 existing subscriptions
|
|
243
|
+
existing_subs = [
|
|
244
|
+
{
|
|
245
|
+
"endpoint": f"https://push.example.com/endpoint{i}",
|
|
246
|
+
"keys": {"p256dh": "key", "auth": "auth"},
|
|
247
|
+
"device_name": f"Device {i}",
|
|
248
|
+
"created_at": f"2024-01-0{i+1}T00:00:00Z"
|
|
249
|
+
}
|
|
250
|
+
for i in range(5)
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
mock_doc = Mock()
|
|
254
|
+
mock_doc.exists = True
|
|
255
|
+
mock_doc.to_dict.return_value = {"push_subscriptions": existing_subs}
|
|
256
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
257
|
+
|
|
258
|
+
result = await push_service.add_subscription(
|
|
259
|
+
"test@example.com",
|
|
260
|
+
"https://push.example.com/new-endpoint",
|
|
261
|
+
{"p256dh": "new-key", "auth": "new-auth"},
|
|
262
|
+
"New Device"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
assert result is True
|
|
266
|
+
# Verify oldest was removed (max 5 subscriptions)
|
|
267
|
+
update_call = push_service.db.collection.return_value.document.return_value.update.call_args
|
|
268
|
+
subs = update_call[0][0]["push_subscriptions"]
|
|
269
|
+
assert len(subs) == 5
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_remove_subscription_success(self, push_service):
|
|
273
|
+
"""remove_subscription removes existing subscription."""
|
|
274
|
+
mock_doc = Mock()
|
|
275
|
+
mock_doc.exists = True
|
|
276
|
+
mock_doc.to_dict.return_value = {
|
|
277
|
+
"push_subscriptions": [{
|
|
278
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
279
|
+
"keys": {"p256dh": "key", "auth": "auth"},
|
|
280
|
+
"device_name": "Test Device"
|
|
281
|
+
}]
|
|
282
|
+
}
|
|
283
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
284
|
+
|
|
285
|
+
result = await push_service.remove_subscription(
|
|
286
|
+
"test@example.com",
|
|
287
|
+
"https://push.example.com/endpoint"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
assert result is True
|
|
291
|
+
update_call = push_service.db.collection.return_value.document.return_value.update.call_args
|
|
292
|
+
subs = update_call[0][0]["push_subscriptions"]
|
|
293
|
+
assert len(subs) == 0
|
|
294
|
+
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_remove_subscription_not_found(self, push_service):
|
|
297
|
+
"""remove_subscription returns False when subscription not found."""
|
|
298
|
+
mock_doc = Mock()
|
|
299
|
+
mock_doc.exists = True
|
|
300
|
+
mock_doc.to_dict.return_value = {"push_subscriptions": []}
|
|
301
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
302
|
+
|
|
303
|
+
result = await push_service.remove_subscription(
|
|
304
|
+
"test@example.com",
|
|
305
|
+
"https://push.example.com/unknown-endpoint"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
assert result is False
|
|
309
|
+
|
|
310
|
+
@pytest.mark.asyncio
|
|
311
|
+
async def test_list_subscriptions_success(self, push_service):
|
|
312
|
+
"""list_subscriptions returns user's subscriptions."""
|
|
313
|
+
mock_doc = Mock()
|
|
314
|
+
mock_doc.exists = True
|
|
315
|
+
mock_doc.to_dict.return_value = {
|
|
316
|
+
"push_subscriptions": [
|
|
317
|
+
{
|
|
318
|
+
"endpoint": "https://push.example.com/endpoint1",
|
|
319
|
+
"keys": {"p256dh": "key", "auth": "auth"},
|
|
320
|
+
"device_name": "Device 1",
|
|
321
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
322
|
+
"last_used_at": None
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"endpoint": "https://push.example.com/endpoint2",
|
|
326
|
+
"keys": {"p256dh": "key", "auth": "auth"},
|
|
327
|
+
"device_name": "Device 2",
|
|
328
|
+
"created_at": "2024-01-02T00:00:00Z",
|
|
329
|
+
"last_used_at": "2024-01-03T00:00:00Z"
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
334
|
+
|
|
335
|
+
result = await push_service.list_subscriptions("test@example.com")
|
|
336
|
+
|
|
337
|
+
assert len(result) == 2
|
|
338
|
+
assert result[0]["device_name"] == "Device 1"
|
|
339
|
+
assert result[1]["device_name"] == "Device 2"
|
|
340
|
+
# Verify keys are NOT included in response (security)
|
|
341
|
+
assert "keys" not in result[0]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class TestNotificationFormatting:
|
|
345
|
+
"""Tests for blocking and completion notification formatting."""
|
|
346
|
+
|
|
347
|
+
@pytest.mark.asyncio
|
|
348
|
+
async def test_send_blocking_notification_lyrics(self, push_service):
|
|
349
|
+
"""send_blocking_notification formats lyrics review notification."""
|
|
350
|
+
mock_doc = Mock()
|
|
351
|
+
mock_doc.exists = True
|
|
352
|
+
mock_doc.to_dict.return_value = {
|
|
353
|
+
"push_subscriptions": [{
|
|
354
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
355
|
+
"keys": {"p256dh": "key", "auth": "auth"}
|
|
356
|
+
}]
|
|
357
|
+
}
|
|
358
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
359
|
+
|
|
360
|
+
job = {
|
|
361
|
+
"job_id": "test-job-123",
|
|
362
|
+
"user_email": "test@example.com",
|
|
363
|
+
"artist": "Test Artist",
|
|
364
|
+
"title": "Test Song"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
with patch('backend.services.push_notification_service.webpush') as mock_webpush:
|
|
368
|
+
await push_service.send_blocking_notification(job, "lyrics")
|
|
369
|
+
|
|
370
|
+
call_args = mock_webpush.call_args
|
|
371
|
+
import json
|
|
372
|
+
payload = json.loads(call_args[1]["data"])
|
|
373
|
+
assert payload["title"] == "Review Lyrics"
|
|
374
|
+
assert "Test Song" in payload["body"]
|
|
375
|
+
assert "Test Artist" in payload["body"]
|
|
376
|
+
assert "/review/test-job-123" in payload["url"]
|
|
377
|
+
|
|
378
|
+
@pytest.mark.asyncio
|
|
379
|
+
async def test_send_blocking_notification_instrumental(self, push_service):
|
|
380
|
+
"""send_blocking_notification formats instrumental selection notification."""
|
|
381
|
+
mock_doc = Mock()
|
|
382
|
+
mock_doc.exists = True
|
|
383
|
+
mock_doc.to_dict.return_value = {
|
|
384
|
+
"push_subscriptions": [{
|
|
385
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
386
|
+
"keys": {"p256dh": "key", "auth": "auth"}
|
|
387
|
+
}]
|
|
388
|
+
}
|
|
389
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
390
|
+
|
|
391
|
+
job = {
|
|
392
|
+
"job_id": "test-job-123",
|
|
393
|
+
"user_email": "test@example.com",
|
|
394
|
+
"artist": "Test Artist",
|
|
395
|
+
"title": "Test Song"
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
with patch('backend.services.push_notification_service.webpush') as mock_webpush:
|
|
399
|
+
await push_service.send_blocking_notification(job, "instrumental")
|
|
400
|
+
|
|
401
|
+
call_args = mock_webpush.call_args
|
|
402
|
+
import json
|
|
403
|
+
payload = json.loads(call_args[1]["data"])
|
|
404
|
+
assert payload["title"] == "Select Instrumental"
|
|
405
|
+
assert "/instrumental/test-job-123" in payload["url"]
|
|
406
|
+
|
|
407
|
+
@pytest.mark.asyncio
|
|
408
|
+
async def test_send_completion_notification(self, push_service):
|
|
409
|
+
"""send_completion_notification formats completion notification."""
|
|
410
|
+
mock_doc = Mock()
|
|
411
|
+
mock_doc.exists = True
|
|
412
|
+
mock_doc.to_dict.return_value = {
|
|
413
|
+
"push_subscriptions": [{
|
|
414
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
415
|
+
"keys": {"p256dh": "key", "auth": "auth"}
|
|
416
|
+
}]
|
|
417
|
+
}
|
|
418
|
+
push_service.db.collection.return_value.document.return_value.get.return_value = mock_doc
|
|
419
|
+
|
|
420
|
+
job = {
|
|
421
|
+
"job_id": "test-job-123",
|
|
422
|
+
"user_email": "test@example.com",
|
|
423
|
+
"artist": "Test Artist",
|
|
424
|
+
"title": "Test Song"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
with patch('backend.services.push_notification_service.webpush') as mock_webpush:
|
|
428
|
+
await push_service.send_completion_notification(job)
|
|
429
|
+
|
|
430
|
+
call_args = mock_webpush.call_args
|
|
431
|
+
import json
|
|
432
|
+
payload = json.loads(call_args[1]["data"])
|
|
433
|
+
assert payload["title"] == "Video Ready!"
|
|
434
|
+
assert "Test Song" in payload["body"]
|
|
435
|
+
assert "Test Artist" in payload["body"]
|
|
436
|
+
assert "download" in payload["body"].lower()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class TestSingleton:
|
|
440
|
+
"""Tests for singleton pattern."""
|
|
441
|
+
|
|
442
|
+
def test_get_push_notification_service_returns_singleton(self):
|
|
443
|
+
"""get_push_notification_service returns same instance."""
|
|
444
|
+
# Reset singleton for test
|
|
445
|
+
import backend.services.push_notification_service as module
|
|
446
|
+
module._push_service = None
|
|
447
|
+
|
|
448
|
+
with patch('backend.services.push_notification_service.get_settings') as mock_get_settings:
|
|
449
|
+
mock_settings = Mock()
|
|
450
|
+
mock_settings.enable_push_notifications = False
|
|
451
|
+
mock_settings.get_secret = Mock(return_value=None)
|
|
452
|
+
mock_get_settings.return_value = mock_settings
|
|
453
|
+
|
|
454
|
+
service1 = get_push_notification_service()
|
|
455
|
+
service2 = get_push_notification_service()
|
|
456
|
+
|
|
457
|
+
assert service1 is service2
|
|
458
|
+
|
|
459
|
+
# Clean up
|
|
460
|
+
module._push_service = None
|