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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Push Notification API routes.
|
|
3
|
+
|
|
4
|
+
Tests the /api/push/* endpoints for subscription management.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch, AsyncMock
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from backend.main import app
|
|
13
|
+
from backend.api.dependencies import require_auth, require_admin
|
|
14
|
+
from backend.services.auth_service import UserType, AuthResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client():
|
|
19
|
+
"""Create test client."""
|
|
20
|
+
return TestClient(app)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_auth_result():
|
|
25
|
+
"""Create a mock AuthResult for regular user."""
|
|
26
|
+
return AuthResult(
|
|
27
|
+
is_valid=True,
|
|
28
|
+
user_type=UserType.UNLIMITED,
|
|
29
|
+
remaining_uses=-1,
|
|
30
|
+
message="Valid",
|
|
31
|
+
user_email="test@example.com",
|
|
32
|
+
is_admin=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def mock_admin_auth_result():
|
|
38
|
+
"""Create a mock AuthResult for admin user."""
|
|
39
|
+
return AuthResult(
|
|
40
|
+
is_valid=True,
|
|
41
|
+
user_type=UserType.ADMIN,
|
|
42
|
+
remaining_uses=-1,
|
|
43
|
+
message="Valid",
|
|
44
|
+
user_email="admin@nomadkaraoke.com",
|
|
45
|
+
is_admin=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestGetVapidPublicKey:
|
|
50
|
+
"""Tests for GET /api/push/vapid-public-key."""
|
|
51
|
+
|
|
52
|
+
def test_returns_disabled_when_feature_off(self, client):
|
|
53
|
+
"""Returns enabled=false when push notifications disabled."""
|
|
54
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
55
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
56
|
+
|
|
57
|
+
response = client.get("/api/push/vapid-public-key")
|
|
58
|
+
|
|
59
|
+
assert response.status_code == 200
|
|
60
|
+
data = response.json()
|
|
61
|
+
assert data["enabled"] is False
|
|
62
|
+
assert data["vapid_public_key"] is None
|
|
63
|
+
|
|
64
|
+
def test_returns_key_when_enabled(self, client):
|
|
65
|
+
"""Returns public key when push notifications enabled."""
|
|
66
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
67
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
68
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
69
|
+
mock_service.return_value.get_public_key.return_value = "test-public-key-123"
|
|
70
|
+
|
|
71
|
+
response = client.get("/api/push/vapid-public-key")
|
|
72
|
+
|
|
73
|
+
assert response.status_code == 200
|
|
74
|
+
data = response.json()
|
|
75
|
+
assert data["enabled"] is True
|
|
76
|
+
assert data["vapid_public_key"] == "test-public-key-123"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestSubscribe:
|
|
80
|
+
"""Tests for POST /api/push/subscribe."""
|
|
81
|
+
|
|
82
|
+
def test_requires_authentication(self, client):
|
|
83
|
+
"""Returns 401 when not authenticated."""
|
|
84
|
+
# Clear any auth overrides to test real auth
|
|
85
|
+
app.dependency_overrides.clear()
|
|
86
|
+
|
|
87
|
+
response = client.post(
|
|
88
|
+
"/api/push/subscribe",
|
|
89
|
+
json={
|
|
90
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
91
|
+
"keys": {"p256dh": "key1", "auth": "key2"}
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert response.status_code == 401
|
|
96
|
+
|
|
97
|
+
def test_returns_503_when_disabled(self, client, mock_auth_result):
|
|
98
|
+
"""Returns 503 when push notifications disabled."""
|
|
99
|
+
async def override_auth():
|
|
100
|
+
return mock_auth_result
|
|
101
|
+
|
|
102
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
106
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
107
|
+
|
|
108
|
+
response = client.post(
|
|
109
|
+
"/api/push/subscribe",
|
|
110
|
+
json={
|
|
111
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
112
|
+
"keys": {"p256dh": "key1", "auth": "key2"}
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert response.status_code == 503
|
|
117
|
+
assert "not enabled" in response.json()["detail"].lower()
|
|
118
|
+
finally:
|
|
119
|
+
app.dependency_overrides.clear()
|
|
120
|
+
|
|
121
|
+
def test_validates_required_keys(self, client, mock_auth_result):
|
|
122
|
+
"""Returns 400 when keys missing."""
|
|
123
|
+
async def override_auth():
|
|
124
|
+
return mock_auth_result
|
|
125
|
+
|
|
126
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
130
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
131
|
+
|
|
132
|
+
response = client.post(
|
|
133
|
+
"/api/push/subscribe",
|
|
134
|
+
json={
|
|
135
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
136
|
+
"keys": {"p256dh": "key1"} # Missing auth
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert response.status_code == 400
|
|
141
|
+
assert "missing" in response.json()["detail"].lower()
|
|
142
|
+
finally:
|
|
143
|
+
app.dependency_overrides.clear()
|
|
144
|
+
|
|
145
|
+
def test_successful_subscription(self, client, mock_auth_result):
|
|
146
|
+
"""Successfully subscribes user to push notifications."""
|
|
147
|
+
async def override_auth():
|
|
148
|
+
return mock_auth_result
|
|
149
|
+
|
|
150
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
154
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
155
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
156
|
+
mock_service.return_value.add_subscription = AsyncMock(return_value=True)
|
|
157
|
+
|
|
158
|
+
response = client.post(
|
|
159
|
+
"/api/push/subscribe",
|
|
160
|
+
json={
|
|
161
|
+
"endpoint": "https://push.example.com/endpoint",
|
|
162
|
+
"keys": {"p256dh": "key1", "auth": "key2"},
|
|
163
|
+
"device_name": "Test Device"
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert response.status_code == 200
|
|
168
|
+
data = response.json()
|
|
169
|
+
assert data["status"] == "success"
|
|
170
|
+
mock_service.return_value.add_subscription.assert_called_once_with(
|
|
171
|
+
user_email="test@example.com",
|
|
172
|
+
endpoint="https://push.example.com/endpoint",
|
|
173
|
+
keys={"p256dh": "key1", "auth": "key2"},
|
|
174
|
+
device_name="Test Device"
|
|
175
|
+
)
|
|
176
|
+
finally:
|
|
177
|
+
app.dependency_overrides.clear()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestUnsubscribe:
|
|
181
|
+
"""Tests for POST /api/push/unsubscribe."""
|
|
182
|
+
|
|
183
|
+
def test_requires_authentication(self, client):
|
|
184
|
+
"""Returns 401 when not authenticated."""
|
|
185
|
+
app.dependency_overrides.clear()
|
|
186
|
+
|
|
187
|
+
response = client.post(
|
|
188
|
+
"/api/push/unsubscribe",
|
|
189
|
+
json={"endpoint": "https://push.example.com/endpoint"}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert response.status_code == 401
|
|
193
|
+
|
|
194
|
+
def test_successful_unsubscribe(self, client, mock_auth_result):
|
|
195
|
+
"""Successfully unsubscribes from push notifications."""
|
|
196
|
+
async def override_auth():
|
|
197
|
+
return mock_auth_result
|
|
198
|
+
|
|
199
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
203
|
+
mock_service.return_value.remove_subscription = AsyncMock(return_value=True)
|
|
204
|
+
|
|
205
|
+
response = client.post(
|
|
206
|
+
"/api/push/unsubscribe",
|
|
207
|
+
json={"endpoint": "https://push.example.com/endpoint"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert response.status_code == 200
|
|
211
|
+
data = response.json()
|
|
212
|
+
assert data["status"] == "success"
|
|
213
|
+
mock_service.return_value.remove_subscription.assert_called_once()
|
|
214
|
+
finally:
|
|
215
|
+
app.dependency_overrides.clear()
|
|
216
|
+
|
|
217
|
+
def test_unsubscribe_not_found_succeeds(self, client, mock_auth_result):
|
|
218
|
+
"""Returns success even when subscription not found."""
|
|
219
|
+
async def override_auth():
|
|
220
|
+
return mock_auth_result
|
|
221
|
+
|
|
222
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
226
|
+
mock_service.return_value.remove_subscription = AsyncMock(return_value=False)
|
|
227
|
+
|
|
228
|
+
response = client.post(
|
|
229
|
+
"/api/push/unsubscribe",
|
|
230
|
+
json={"endpoint": "https://push.example.com/unknown"}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert response.status_code == 200
|
|
234
|
+
data = response.json()
|
|
235
|
+
assert data["status"] == "success"
|
|
236
|
+
finally:
|
|
237
|
+
app.dependency_overrides.clear()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestListSubscriptions:
|
|
241
|
+
"""Tests for GET /api/push/subscriptions."""
|
|
242
|
+
|
|
243
|
+
def test_requires_authentication(self, client):
|
|
244
|
+
"""Returns 401 when not authenticated."""
|
|
245
|
+
app.dependency_overrides.clear()
|
|
246
|
+
|
|
247
|
+
response = client.get("/api/push/subscriptions")
|
|
248
|
+
assert response.status_code == 401
|
|
249
|
+
|
|
250
|
+
def test_returns_user_subscriptions(self, client, mock_auth_result):
|
|
251
|
+
"""Returns list of user's subscriptions."""
|
|
252
|
+
async def override_auth():
|
|
253
|
+
return mock_auth_result
|
|
254
|
+
|
|
255
|
+
app.dependency_overrides[require_auth] = override_auth
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
with patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
259
|
+
mock_service.return_value.list_subscriptions = AsyncMock(return_value=[
|
|
260
|
+
{
|
|
261
|
+
"endpoint": "https://push.example.com/endpoint1",
|
|
262
|
+
"device_name": "Device 1",
|
|
263
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
264
|
+
"last_used_at": None
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"endpoint": "https://push.example.com/endpoint2",
|
|
268
|
+
"device_name": "Device 2",
|
|
269
|
+
"created_at": "2024-01-02T00:00:00Z",
|
|
270
|
+
"last_used_at": "2024-01-03T00:00:00Z"
|
|
271
|
+
}
|
|
272
|
+
])
|
|
273
|
+
|
|
274
|
+
response = client.get("/api/push/subscriptions")
|
|
275
|
+
|
|
276
|
+
assert response.status_code == 200
|
|
277
|
+
data = response.json()
|
|
278
|
+
assert data["count"] == 2
|
|
279
|
+
assert len(data["subscriptions"]) == 2
|
|
280
|
+
assert data["subscriptions"][0]["device_name"] == "Device 1"
|
|
281
|
+
finally:
|
|
282
|
+
app.dependency_overrides.clear()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestTestNotification:
|
|
286
|
+
"""Tests for POST /api/push/test."""
|
|
287
|
+
|
|
288
|
+
def test_requires_admin(self, client, mock_auth_result):
|
|
289
|
+
"""Returns 403 when not admin."""
|
|
290
|
+
# Use a non-admin auth result
|
|
291
|
+
async def override_admin():
|
|
292
|
+
from fastapi import HTTPException
|
|
293
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
294
|
+
|
|
295
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
response = client.post(
|
|
299
|
+
"/api/push/test",
|
|
300
|
+
json={"title": "Test", "body": "Test message"}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
assert response.status_code == 403
|
|
304
|
+
finally:
|
|
305
|
+
app.dependency_overrides.clear()
|
|
306
|
+
|
|
307
|
+
def test_returns_503_when_disabled(self, client, mock_admin_auth_result):
|
|
308
|
+
"""Returns 503 when push notifications disabled."""
|
|
309
|
+
async def override_admin():
|
|
310
|
+
return mock_admin_auth_result
|
|
311
|
+
|
|
312
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings:
|
|
316
|
+
mock_settings.return_value.enable_push_notifications = False
|
|
317
|
+
|
|
318
|
+
response = client.post(
|
|
319
|
+
"/api/push/test",
|
|
320
|
+
json={"title": "Test", "body": "Test message"}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert response.status_code == 503
|
|
324
|
+
finally:
|
|
325
|
+
app.dependency_overrides.clear()
|
|
326
|
+
|
|
327
|
+
def test_sends_test_notification(self, client, mock_admin_auth_result):
|
|
328
|
+
"""Successfully sends test notification to admin."""
|
|
329
|
+
async def override_admin():
|
|
330
|
+
return mock_admin_auth_result
|
|
331
|
+
|
|
332
|
+
app.dependency_overrides[require_admin] = override_admin
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
with patch('backend.api.routes.push.get_settings') as mock_settings, \
|
|
336
|
+
patch('backend.api.routes.push.get_push_notification_service') as mock_service:
|
|
337
|
+
mock_settings.return_value.enable_push_notifications = True
|
|
338
|
+
mock_service.return_value.send_push = AsyncMock(return_value=2)
|
|
339
|
+
|
|
340
|
+
response = client.post(
|
|
341
|
+
"/api/push/test",
|
|
342
|
+
json={"title": "Test Title", "body": "Test Body"}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
assert response.status_code == 200
|
|
346
|
+
data = response.json()
|
|
347
|
+
assert data["status"] == "success"
|
|
348
|
+
assert data["sent_count"] == 2
|
|
349
|
+
mock_service.return_value.send_push.assert_called_once_with(
|
|
350
|
+
user_email="admin@nomadkaraoke.com",
|
|
351
|
+
title="Test Title",
|
|
352
|
+
body="Test Body",
|
|
353
|
+
url="/app/",
|
|
354
|
+
tag="test"
|
|
355
|
+
)
|
|
356
|
+
finally:
|
|
357
|
+
app.dependency_overrides.clear()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for StripeService.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Made-for-you checkout session URL generation
|
|
6
|
+
- Credit purchase checkout session URL generation
|
|
7
|
+
- Package validation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestStripeServiceUrls:
|
|
16
|
+
"""Tests for checkout session URL generation."""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def stripe_service(self):
|
|
20
|
+
"""Create a StripeService instance with mocked Stripe."""
|
|
21
|
+
# Set required env vars
|
|
22
|
+
with patch.dict(os.environ, {
|
|
23
|
+
'STRIPE_SECRET_KEY': 'sk_test_fake',
|
|
24
|
+
'FRONTEND_URL': 'https://gen.nomadkaraoke.com',
|
|
25
|
+
}):
|
|
26
|
+
# Import here to pick up env vars
|
|
27
|
+
from backend.services.stripe_service import StripeService
|
|
28
|
+
service = StripeService()
|
|
29
|
+
return service
|
|
30
|
+
|
|
31
|
+
def test_made_for_you_success_url_uses_frontend_url(self, stripe_service):
|
|
32
|
+
"""Test that made-for-you checkout uses frontend_url, not hardcoded domain."""
|
|
33
|
+
# Mock stripe.checkout.Session.create to capture the params
|
|
34
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
35
|
+
mock_session = MagicMock()
|
|
36
|
+
mock_session.id = 'cs_test_123'
|
|
37
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
38
|
+
mock_create.return_value = mock_session
|
|
39
|
+
|
|
40
|
+
success, url, message = stripe_service.create_made_for_you_checkout_session(
|
|
41
|
+
customer_email='customer@example.com',
|
|
42
|
+
artist='Test Artist',
|
|
43
|
+
title='Test Song',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Verify the call was made
|
|
47
|
+
assert mock_create.called
|
|
48
|
+
call_kwargs = mock_create.call_args[1]
|
|
49
|
+
|
|
50
|
+
# Verify success_url uses frontend_url and correct path
|
|
51
|
+
success_url = call_kwargs['success_url']
|
|
52
|
+
assert success_url.startswith('https://gen.nomadkaraoke.com')
|
|
53
|
+
assert '/order/success' in success_url
|
|
54
|
+
assert 'session_id=' in success_url
|
|
55
|
+
|
|
56
|
+
def test_made_for_you_success_url_not_hardcoded_to_marketing_site(self, stripe_service):
|
|
57
|
+
"""Test that success_url is NOT the old hardcoded marketing site URL."""
|
|
58
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
59
|
+
mock_session = MagicMock()
|
|
60
|
+
mock_session.id = 'cs_test_123'
|
|
61
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
62
|
+
mock_create.return_value = mock_session
|
|
63
|
+
|
|
64
|
+
stripe_service.create_made_for_you_checkout_session(
|
|
65
|
+
customer_email='customer@example.com',
|
|
66
|
+
artist='Test Artist',
|
|
67
|
+
title='Test Song',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
call_kwargs = mock_create.call_args[1]
|
|
71
|
+
success_url = call_kwargs['success_url']
|
|
72
|
+
|
|
73
|
+
# The bug was: success_url = "https://nomadkaraoke.com/order/success/"
|
|
74
|
+
# (note: nomadkaraoke.com WITHOUT the 'gen.' prefix - that's the marketing site)
|
|
75
|
+
# This should NOT be the case anymore - should use gen.nomadkaraoke.com
|
|
76
|
+
assert not success_url.startswith('https://nomadkaraoke.com/')
|
|
77
|
+
# Should use the frontend URL (gen.nomadkaraoke.com)
|
|
78
|
+
assert success_url.startswith('https://gen.nomadkaraoke.com')
|
|
79
|
+
|
|
80
|
+
def test_made_for_you_respects_custom_frontend_url(self):
|
|
81
|
+
"""Test that made-for-you checkout uses custom FRONTEND_URL if set."""
|
|
82
|
+
with patch.dict(os.environ, {
|
|
83
|
+
'STRIPE_SECRET_KEY': 'sk_test_fake',
|
|
84
|
+
'FRONTEND_URL': 'https://custom.example.com',
|
|
85
|
+
}):
|
|
86
|
+
from backend.services.stripe_service import StripeService
|
|
87
|
+
service = StripeService()
|
|
88
|
+
|
|
89
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
90
|
+
mock_session = MagicMock()
|
|
91
|
+
mock_session.id = 'cs_test_123'
|
|
92
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
93
|
+
mock_create.return_value = mock_session
|
|
94
|
+
|
|
95
|
+
service.create_made_for_you_checkout_session(
|
|
96
|
+
customer_email='customer@example.com',
|
|
97
|
+
artist='Test Artist',
|
|
98
|
+
title='Test Song',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
call_kwargs = mock_create.call_args[1]
|
|
102
|
+
success_url = call_kwargs['success_url']
|
|
103
|
+
assert success_url.startswith('https://custom.example.com')
|
|
104
|
+
|
|
105
|
+
def test_credit_purchase_success_url_uses_payment_success(self, stripe_service):
|
|
106
|
+
"""Test that credit purchase checkout uses /payment/success path."""
|
|
107
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
108
|
+
mock_session = MagicMock()
|
|
109
|
+
mock_session.id = 'cs_test_123'
|
|
110
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
111
|
+
mock_create.return_value = mock_session
|
|
112
|
+
|
|
113
|
+
success, url, message = stripe_service.create_checkout_session(
|
|
114
|
+
package_id='1_credit',
|
|
115
|
+
user_email='user@example.com',
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
call_kwargs = mock_create.call_args[1]
|
|
119
|
+
success_url = call_kwargs['success_url']
|
|
120
|
+
|
|
121
|
+
# Credit purchases go to /payment/success
|
|
122
|
+
assert '/payment/success' in success_url
|
|
123
|
+
assert success_url.startswith('https://gen.nomadkaraoke.com')
|
|
124
|
+
|
|
125
|
+
def test_made_for_you_uses_order_success_path(self, stripe_service):
|
|
126
|
+
"""Test that made-for-you uses /order/success path (not /payment/success)."""
|
|
127
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
128
|
+
mock_session = MagicMock()
|
|
129
|
+
mock_session.id = 'cs_test_123'
|
|
130
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
131
|
+
mock_create.return_value = mock_session
|
|
132
|
+
|
|
133
|
+
stripe_service.create_made_for_you_checkout_session(
|
|
134
|
+
customer_email='customer@example.com',
|
|
135
|
+
artist='Test Artist',
|
|
136
|
+
title='Test Song',
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
call_kwargs = mock_create.call_args[1]
|
|
140
|
+
success_url = call_kwargs['success_url']
|
|
141
|
+
|
|
142
|
+
# Made-for-you orders go to /order/success (different messaging)
|
|
143
|
+
assert '/order/success' in success_url
|
|
144
|
+
# Should NOT go to /payment/success
|
|
145
|
+
assert '/payment/success' not in success_url
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestStripeServiceMetadata:
|
|
149
|
+
"""Tests for checkout session metadata."""
|
|
150
|
+
|
|
151
|
+
@pytest.fixture
|
|
152
|
+
def stripe_service(self):
|
|
153
|
+
"""Create a StripeService instance with mocked Stripe."""
|
|
154
|
+
with patch.dict(os.environ, {
|
|
155
|
+
'STRIPE_SECRET_KEY': 'sk_test_fake',
|
|
156
|
+
'FRONTEND_URL': 'https://gen.nomadkaraoke.com',
|
|
157
|
+
}):
|
|
158
|
+
from backend.services.stripe_service import StripeService
|
|
159
|
+
return StripeService()
|
|
160
|
+
|
|
161
|
+
def test_made_for_you_includes_order_type_metadata(self, stripe_service):
|
|
162
|
+
"""Test that made-for-you checkout includes order_type in metadata."""
|
|
163
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
164
|
+
mock_session = MagicMock()
|
|
165
|
+
mock_session.id = 'cs_test_123'
|
|
166
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
167
|
+
mock_create.return_value = mock_session
|
|
168
|
+
|
|
169
|
+
stripe_service.create_made_for_you_checkout_session(
|
|
170
|
+
customer_email='customer@example.com',
|
|
171
|
+
artist='Test Artist',
|
|
172
|
+
title='Test Song',
|
|
173
|
+
notes='Special request',
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
call_kwargs = mock_create.call_args[1]
|
|
177
|
+
metadata = call_kwargs['metadata']
|
|
178
|
+
|
|
179
|
+
assert metadata['order_type'] == 'made_for_you'
|
|
180
|
+
assert metadata['customer_email'] == 'customer@example.com'
|
|
181
|
+
assert metadata['artist'] == 'Test Artist'
|
|
182
|
+
assert metadata['title'] == 'Test Song'
|
|
183
|
+
assert metadata['notes'] == 'Special request'
|
|
184
|
+
|
|
185
|
+
def test_made_for_you_truncates_long_notes(self, stripe_service):
|
|
186
|
+
"""Test that notes longer than 500 chars are truncated."""
|
|
187
|
+
with patch('stripe.checkout.Session.create') as mock_create:
|
|
188
|
+
mock_session = MagicMock()
|
|
189
|
+
mock_session.id = 'cs_test_123'
|
|
190
|
+
mock_session.url = 'https://checkout.stripe.com/test'
|
|
191
|
+
mock_create.return_value = mock_session
|
|
192
|
+
|
|
193
|
+
long_notes = 'x' * 600
|
|
194
|
+
|
|
195
|
+
stripe_service.create_made_for_you_checkout_session(
|
|
196
|
+
customer_email='customer@example.com',
|
|
197
|
+
artist='Test Artist',
|
|
198
|
+
title='Test Song',
|
|
199
|
+
notes=long_notes,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
call_kwargs = mock_create.call_args[1]
|
|
203
|
+
metadata = call_kwargs['metadata']
|
|
204
|
+
|
|
205
|
+
assert len(metadata['notes']) == 500
|