import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NextRequest } from 'next/server'; const mocks = vi.hoisted(() => ({ cancelJob: vi.fn(), deleteClassroom: vi.fn(), deleteJob: vi.fn(), isPublishedSource: vi.fn(), readJob: vi.fn(), })); vi.mock('@/lib/server/classroom-job-store', () => ({ deleteClassroomGenerationJob: mocks.deleteJob, isValidClassroomJobId: (value: string) => /^[a-zA-Z0-9_-]+$/.test(value), markClassroomGenerationJobCancelled: vi.fn(), readClassroomGenerationJob: mocks.readJob, })); vi.mock('@/lib/server/classroom-job-runner', () => ({ cancelClassroomGenerationJob: mocks.cancelJob, })); vi.mock('@/lib/server/classroom-storage', () => ({ buildRequestOrigin: () => 'https://server.example', deleteClassroom: mocks.deleteClassroom, })); vi.mock('@/lib/server/published-classroom-access', () => ({ isPublishedClassroomSource: mocks.isPublishedSource, })); describe('DELETE /api/generate-classroom/[jobId]', () => { beforeEach(() => { vi.clearAllMocks(); mocks.readJob.mockResolvedValue({ id: 'job-1', status: 'succeeded', result: { classroomId: 'classroom-1', url: '/classroom/classroom-1', scenesCount: 2 }, }); mocks.deleteClassroom.mockResolvedValue(true); mocks.deleteJob.mockResolvedValue(true); }); test('preserves both job and classroom when the source has been published', async () => { mocks.isPublishedSource.mockResolvedValue(true); const { DELETE } = await import('@/app/api/generate-classroom/[jobId]/route'); const response = await DELETE( new NextRequest('https://server.example/api/generate-classroom/job-1', { method: 'DELETE', }), { params: Promise.resolve({ jobId: 'job-1' }) }, ); expect(response.status).toBe(409); expect(mocks.cancelJob).not.toHaveBeenCalled(); expect(mocks.deleteClassroom).not.toHaveBeenCalled(); expect(mocks.deleteJob).not.toHaveBeenCalled(); }); test('keeps the existing delete behavior for an unpublished source', async () => { mocks.isPublishedSource.mockResolvedValue(false); const { DELETE } = await import('@/app/api/generate-classroom/[jobId]/route'); const response = await DELETE( new NextRequest('https://server.example/api/generate-classroom/job-1', { method: 'DELETE', }), { params: Promise.resolve({ jobId: 'job-1' }) }, ); expect(response.status).toBe(200); expect(mocks.cancelJob).toHaveBeenCalledWith('job-1'); expect(mocks.deleteClassroom).toHaveBeenCalledWith('classroom-1'); expect(mocks.deleteJob).toHaveBeenCalledWith('job-1'); }); test('returns 409 without deleting while the classroom source is being published', async () => { mocks.isPublishedSource.mockResolvedValue(false); const { runClassroomSourcesPublishExclusive } = await import('@/lib/course-framework/publish-state'); let finishPublish!: () => void; const publishing = runClassroomSourcesPublishExclusive( ['classroom-1'], () => new Promise((resolve) => { finishPublish = resolve; }), ); const { DELETE } = await import('@/app/api/generate-classroom/[jobId]/route'); const response = await DELETE( new NextRequest('https://server.example/api/generate-classroom/job-1', { method: 'DELETE', }), { params: Promise.resolve({ jobId: 'job-1' }) }, ); expect(response.status).toBe(409); expect(mocks.isPublishedSource).not.toHaveBeenCalled(); expect(mocks.cancelJob).not.toHaveBeenCalled(); expect(mocks.deleteClassroom).not.toHaveBeenCalled(); expect(mocks.deleteJob).not.toHaveBeenCalled(); finishPublish(); await publishing; }); });