chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,175 @@
|
||||
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(),
|
||||
markCancelled: vi.fn(),
|
||||
authorizeAccess: vi.fn(),
|
||||
accessError: vi.fn(),
|
||||
readJob: vi.fn(),
|
||||
resumeJob: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/classroom-job-store', () => ({
|
||||
deleteClassroomGenerationJob: mocks.deleteJob,
|
||||
isValidClassroomJobId: (value: string) => /^[a-zA-Z0-9_-]+$/.test(value),
|
||||
markClassroomGenerationJobCancelled: mocks.markCancelled,
|
||||
readClassroomGenerationJob: mocks.readJob,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/classroom-job-runner', () => ({
|
||||
cancelClassroomGenerationJob: mocks.cancelJob,
|
||||
resumeClassroomGenerationJob: mocks.resumeJob,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/classroom-storage', () => ({
|
||||
buildRequestOrigin: () => 'https://server.example',
|
||||
deleteClassroom: mocks.deleteClassroom,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/published-classroom-access', () => ({
|
||||
isPublishedClassroomSource: mocks.isPublishedSource,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/authz/classroom-job-access', () => ({
|
||||
authorizeClassroomJobRequest: mocks.authorizeAccess,
|
||||
classroomJobAccessError: mocks.accessError,
|
||||
}));
|
||||
|
||||
function storedJob(status: 'queued' | 'failed' = 'failed') {
|
||||
return {
|
||||
id: 'job-1',
|
||||
status,
|
||||
step: status,
|
||||
progress: status === 'failed' ? 100 : 0,
|
||||
message: status,
|
||||
createdAt: '2026-08-15T00:00:00.000Z',
|
||||
updatedAt: '2026-08-15T00:00:00.000Z',
|
||||
inputSummary: {
|
||||
requirementPreview: 'topic',
|
||||
hasPdf: false,
|
||||
pdfTextLength: 0,
|
||||
pdfImageCount: 0,
|
||||
},
|
||||
input: { requirement: 'topic' },
|
||||
scenesGenerated: 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe('classroom job observe-only route seam', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.authorizeAccess.mockResolvedValue({ allowed: true });
|
||||
mocks.accessError.mockImplementation(
|
||||
(denial: { status: number; code: string; message: string }) =>
|
||||
Response.json(
|
||||
{ success: false, errorCode: denial.code, error: denial.message },
|
||||
{ status: denial.status },
|
||||
),
|
||||
);
|
||||
mocks.deleteJob.mockResolvedValue(true);
|
||||
mocks.resumeJob.mockResolvedValue(true);
|
||||
mocks.isPublishedSource.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
test('observes detail without changing its response', async () => {
|
||||
const job = storedJob();
|
||||
mocks.readJob.mockResolvedValue(job);
|
||||
const { GET } = await import('@/app/api/generate-classroom/[jobId]/route');
|
||||
const request = new NextRequest('https://server.example/api/generate-classroom/job-1');
|
||||
|
||||
const response = await GET(request, { params: Promise.resolve({ jobId: 'job-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
success: true,
|
||||
jobId: 'job-1',
|
||||
status: 'failed',
|
||||
done: true,
|
||||
});
|
||||
expect(mocks.authorizeAccess).toHaveBeenCalledWith(request, job, 'read');
|
||||
});
|
||||
|
||||
test('observes cancel without changing its response', async () => {
|
||||
const job = storedJob('queued');
|
||||
mocks.readJob.mockResolvedValue(job);
|
||||
const { POST } = await import('@/app/api/generate-classroom/[jobId]/route');
|
||||
const request = new NextRequest('https://server.example/api/generate-classroom/job-1', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: Promise.resolve({ jobId: 'job-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
success: true,
|
||||
jobId: 'job-1',
|
||||
status: 'cancelling',
|
||||
});
|
||||
expect(mocks.authorizeAccess).toHaveBeenCalledWith(request, job, 'cancel');
|
||||
expect(mocks.markCancelled).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('observes delete without changing its response', async () => {
|
||||
const job = storedJob();
|
||||
mocks.readJob.mockResolvedValue(job);
|
||||
const { DELETE } = await import('@/app/api/generate-classroom/[jobId]/route');
|
||||
const request = new NextRequest('https://server.example/api/generate-classroom/job-1', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const response = await DELETE(request, { params: Promise.resolve({ jobId: 'job-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
success: true,
|
||||
jobId: 'job-1',
|
||||
deleted: true,
|
||||
});
|
||||
expect(mocks.authorizeAccess).toHaveBeenCalledWith(request, job, 'delete');
|
||||
expect(mocks.deleteJob).toHaveBeenCalledWith('job-1');
|
||||
});
|
||||
|
||||
test('observes resume without changing its response', async () => {
|
||||
const job = storedJob();
|
||||
mocks.readJob.mockResolvedValue(job);
|
||||
const { POST } = await import('@/app/api/generate-classroom/[jobId]/resume/route');
|
||||
const request = new NextRequest('https://server.example/api/generate-classroom/job-1/resume', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: Promise.resolve({ jobId: 'job-1' }) });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
success: true,
|
||||
jobId: 'job-1',
|
||||
status: 'queued',
|
||||
});
|
||||
expect(mocks.authorizeAccess).toHaveBeenCalledWith(request, job, 'resume');
|
||||
});
|
||||
|
||||
test('returns the authorization denial before cancelling or mutating a job', async () => {
|
||||
const job = storedJob('queued');
|
||||
mocks.readJob.mockResolvedValue(job);
|
||||
mocks.authorizeAccess.mockResolvedValue({
|
||||
allowed: false,
|
||||
status: 403,
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You do not have access to this resource.',
|
||||
});
|
||||
const { POST } = await import('@/app/api/generate-classroom/[jobId]/route');
|
||||
const request = new NextRequest('https://server.example/api/generate-classroom/job-1', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: Promise.resolve({ jobId: 'job-1' }) });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(mocks.cancelJob).not.toHaveBeenCalled();
|
||||
expect(mocks.markCancelled).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user