import { beforeEach, describe, expect, it, vi } from 'vitest'; import { db } from '@/lib/db'; import { GET as listVideos, POST as addVideo } from '@/app/api/projects/[projectId]/videos/route'; import { POST as bulkDelete } from '@/app/api/projects/[projectId]/videos/bulk-delete/route'; import { GET as listMoveTargets, POST as moveVideos, } from '@/app/api/projects/[projectId]/videos/move/route'; import { DELETE as cancelR2Upload, POST as initR2Upload, } from '@/app/api/projects/[projectId]/videos/r2-init/route'; import { POST as completeR2Upload } from '@/app/api/projects/[projectId]/videos/r2-complete/route'; import { apiRequest, callRoute, readData } from '../helpers/request'; import { signedInAs, signedOut } from '../helpers/session'; import { addProjectMember, addWorkspaceMember, createProject, createShareLink, createUser, createVersion, createVideo, createWorkspace, seedProject, seedVersion, } from '../factories'; function videosUrl(projectId: string): string { return `/api/projects/${projectId}/videos`; } /** Turns on the self-hosted direct-upload path for a single test. */ function enableS3VideoUploads(): void { vi.stubEnv('OPENFRAME_ENABLE_S3_VIDEO_UPLOADS', 'true'); vi.stubEnv('OPENFRAME_ENABLE_BUNNY_UPLOADS', 'false'); vi.stubEnv('R2_ACCESS_KEY_ID', 'test-access-key'); vi.stubEnv('R2_SECRET_ACCESS_KEY', 'test-secret-key'); vi.stubEnv('R2_BUCKET_NAME', 'openframe-test'); vi.stubEnv('R2_ACCOUNT_ID', 'test-account'); } describe('GET /api/projects/[projectId]/videos', () => { it('returns 404 for an unknown project', async () => { const user = await createUser(); signedInAs(user); const response = await callRoute(listVideos, apiRequest(videosUrl('nope')), { projectId: 'nope', }); expect(response.status).toBe(404); }); it('returns 403 to an anonymous caller on a PRIVATE project', async () => { const scenario = await seedProject({ visibility: 'PRIVATE' }); signedOut(); const response = await callRoute(listVideos, apiRequest(videosUrl(scenario.project.id)), { projectId: scenario.project.id, }); expect(response.status).toBe(403); }); it('lists videos in position order with the active version only', async () => { const scenario = await seedProject(); const second = await createVideo({ projectId: scenario.project.id, position: 1 }); const first = await createVideo({ projectId: scenario.project.id, position: 0 }); await createVersion({ videoParentId: first.id, versionNumber: 1, isActive: false }); const active = await createVersion({ videoParentId: first.id, versionNumber: 2, isActive: true, }); signedInAs(scenario.owner); const payload = await readData<{ videos: Array<{ id: string; versions: Array<{ id: string }>; _count: { versions: number } }>; }>( await callRoute(listVideos, apiRequest(videosUrl(scenario.project.id)), { projectId: scenario.project.id, }) ); expect(payload.videos.map((entry) => entry.id)).toEqual([first.id, second.id]); expect(payload.videos[0].versions.map((entry) => entry.id)).toEqual([active.id]); expect(payload.videos[0]._count.versions).toBe(2); }); }); describe('POST /api/projects/[projectId]/videos', () => { it('returns 401 without a session', async () => { const scenario = await seedProject(); signedOut(); const response = await callRoute( addVideo, apiRequest(videosUrl(scenario.project.id), { body: { title: 'X', videoUrl: 'https://www.youtube.com/watch?v=abc' }, }), { projectId: scenario.project.id } ); expect(response.status).toBe(401); expect(await db.video.count()).toBe(0); }); it('returns 403 for a project COMMENTATOR', async () => { const scenario = await seedProject(); const commentator = await createUser(); await addProjectMember({ projectId: scenario.project.id, userId: commentator.id, role: 'COMMENTATOR', }); signedInAs(commentator); const response = await callRoute( addVideo, apiRequest(videosUrl(scenario.project.id), { body: { title: 'X', videoUrl: 'https://www.youtube.com/watch?v=abc' }, }), { projectId: scenario.project.id } ); expect(response.status).toBe(403); expect(await db.video.count()).toBe(0); }); it.each([ [{ videoUrl: 'https://www.youtube.com/watch?v=abc' }, 'a missing title'], [{ title: 'X' }, 'a missing videoUrl'], [{ title: 'X', videoUrl: 'javascript:alert(1)' }, 'a javascript: URL'], [{ title: 'X', videoUrl: 'data:text/html,