chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,153 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user