Files
openmaic/OpenMAIC/tests/server/classroom-job-create-list-authz-route.test.ts
2026-08-16 14:58:47 +08:00

154 lines
5.0 KiB
TypeScript

import { beforeEach, describe, expect, test, vi } from 'vitest';
import { NextRequest } from 'next/server';
const mocks = vi.hoisted(() => ({
after: vi.fn(),
authorizeCreation: vi.fn(),
authorizeList: vi.fn(),
accessError: vi.fn(),
createJob: vi.fn(),
listJobs: vi.fn(),
runJob: vi.fn(),
}));
vi.mock('next/server', async (importOriginal) => {
const actual = await importOriginal<typeof import('next/server')>();
return { ...actual, after: mocks.after };
});
vi.mock('nanoid', () => ({ nanoid: () => 'job-created' }));
vi.mock('@/lib/server/classroom-job-store', () => ({
createClassroomGenerationJob: mocks.createJob,
listClassroomGenerationJobs: mocks.listJobs,
}));
vi.mock('@/lib/server/classroom-job-runner', () => ({
runClassroomGenerationJob: mocks.runJob,
}));
vi.mock('@/lib/server/classroom-storage', () => ({
buildRequestOrigin: () => 'https://server.example',
}));
vi.mock('@/lib/server/authz/classroom-job-access', () => ({
authorizeClassroomJobCreation: mocks.authorizeCreation,
authorizeClassroomJobList: mocks.authorizeList,
classroomJobAccessError: mocks.accessError,
}));
function storedJob(id: string) {
return {
id,
status: 'queued',
step: 'queued',
progress: 0,
message: 'queued',
createdAt: '2026-08-16T00:00:00.000Z',
updatedAt: '2026-08-16T00:00:00.000Z',
inputSummary: {
requirementPreview: id,
hasPdf: false,
pdfTextLength: 0,
pdfImageCount: 0,
},
input: { requirement: id },
scenesGenerated: 0,
};
}
describe('generation job create/list authorization boundary', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
mocks.accessError.mockImplementation(
(denial: { status: number; code: string; message: string }) =>
Response.json(
{ success: false, errorCode: denial.code, error: denial.message },
{ status: denial.status },
),
);
mocks.authorizeCreation.mockResolvedValue({
allowed: true,
ownership: { ownerPrincipalId: 'account-1' },
});
mocks.createJob.mockResolvedValue(storedJob('job-created'));
mocks.listJobs.mockResolvedValue([storedJob('mine'), storedJob('other')]);
mocks.authorizeList.mockImplementation(async (_request, jobs) => ({
allowed: true,
jobs: [jobs[0]],
}));
});
test('binds the server-derived owner when creating a public generation job', async () => {
const { POST } = await import('@/app/api/generate-classroom/route');
const request = new NextRequest('https://server.example/api/generate-classroom', {
method: 'POST',
body: JSON.stringify({ requirement: 'Algebra' }),
headers: { 'content-type': 'application/json' },
});
const response = await POST(request);
expect(response.status).toBe(202);
expect(mocks.createJob).toHaveBeenCalledWith(
'job-created',
expect.objectContaining({ requirement: 'Algebra' }),
{ ownerPrincipalId: 'account-1' },
);
expect(mocks.after).toHaveBeenCalledOnce();
});
test('returns a required-mode denial before creating or scheduling work', async () => {
mocks.authorizeCreation.mockResolvedValue({
allowed: false,
status: 503,
code: 'IDENTITY_UNAVAILABLE',
message: 'Identity verification is unavailable.',
});
const { POST } = await import('@/app/api/generate-classroom/route');
const request = new NextRequest('https://server.example/api/generate-classroom', {
method: 'POST',
body: JSON.stringify({ requirement: 'Algebra' }),
headers: { 'content-type': 'application/json' },
});
const response = await POST(request);
expect(response.status).toBe(503);
expect(mocks.createJob).not.toHaveBeenCalled();
expect(mocks.after).not.toHaveBeenCalled();
});
test('returns only the jobs selected by required-mode ownership filtering', async () => {
const { GET } = await import('@/app/api/generate-classroom/route');
const response = await GET(
new NextRequest('https://server.example/api/generate-classroom?limit=20'),
);
const body = await response.json();
expect(response.status).toBe(200);
expect(body.items).toHaveLength(1);
expect(body.items[0].jobId).toBe('mine');
expect(mocks.authorizeList).toHaveBeenCalledWith(expect.any(NextRequest), expect.any(Array), {
mode: 'shadow',
});
});
test('loads before owner filtering and applies the user limit afterwards in required mode', async () => {
vi.stubEnv('OPENMAIC_IDENTITY_ENFORCEMENT', 'required');
const { GET } = await import('@/app/api/generate-classroom/route');
const response = await GET(
new NextRequest('https://server.example/api/generate-classroom?limit=1'),
);
const body = await response.json();
expect(response.status).toBe(200);
expect(mocks.listJobs).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER);
expect(mocks.authorizeList).toHaveBeenCalledWith(expect.any(NextRequest), expect.any(Array), {
mode: 'required',
});
expect(body.items).toHaveLength(1);
});
});