Files
openmaic/OpenMAIC/tests/document/extract-document-route.test.ts
2026-08-16 14:58:47 +08:00

261 lines
8.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { NextRequest } from 'next/server';
const mocks = vi.hoisted(() => ({
isServerConfiguredProvider: vi.fn(() => false),
requireProviderAccess: vi.fn(),
resolveManagedAliDocMindCredentials: vi.fn(),
resolvePDFApiKey: vi.fn((_providerId: string, clientKey?: string) => clientKey || ''),
resolvePDFBaseUrl: vi.fn((_providerId: string, clientBaseUrl?: string) => clientBaseUrl),
parseWithMinerUCloud: vi.fn(),
extractMedia: vi.fn(),
}));
vi.mock('@/lib/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock('@/lib/server/provider-config', () => ({
isServerConfiguredProvider: mocks.isServerConfiguredProvider,
requireProviderAccess: mocks.requireProviderAccess,
resolveManagedAliDocMindCredentials: mocks.resolveManagedAliDocMindCredentials,
resolvePDFApiKey: mocks.resolvePDFApiKey,
resolvePDFBaseUrl: mocks.resolvePDFBaseUrl,
}));
vi.mock('@/lib/document', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/document')>();
return {
...actual,
extractMedia: mocks.extractMedia,
};
});
vi.mock('@/lib/pdf/mineru-cloud', () => ({
parseWithMinerUCloud: mocks.parseWithMinerUCloud,
}));
async function postExtractDocument(input: {
file: File;
providerId?: string;
apiKey?: string;
baseUrl?: string;
accessKeyId?: string;
accessKeySecret?: string;
}) {
const { POST } = await import('@/app/api/extract-document/route');
const formData = new FormData();
formData.append('file', input.file);
if (input.providerId) formData.append('providerId', input.providerId);
if (input.apiKey) formData.append('apiKey', input.apiKey);
if (input.baseUrl) formData.append('baseUrl', input.baseUrl);
if (input.accessKeyId) formData.append('accessKeyId', input.accessKeyId);
if (input.accessKeySecret) formData.append('accessKeySecret', input.accessKeySecret);
const request = new Request('http://localhost/api/extract-document', {
method: 'POST',
body: formData,
});
return POST(request as unknown as NextRequest);
}
describe('POST /api/extract-document', () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllEnvs();
mocks.isServerConfiguredProvider.mockReturnValue(false);
mocks.requireProviderAccess.mockReset();
mocks.resolveManagedAliDocMindCredentials.mockReset();
mocks.resolvePDFApiKey.mockImplementation(
(_providerId: string, clientKey?: string) => clientKey || '',
);
mocks.resolvePDFBaseUrl.mockImplementation(
(_providerId: string, clientBaseUrl?: string) => clientBaseUrl,
);
mocks.parseWithMinerUCloud.mockReset();
mocks.extractMedia.mockReset();
mocks.parseWithMinerUCloud.mockResolvedValue({
text: 'cloud parsed text',
images: [],
metadata: {
pageCount: 1,
parser: 'mineru-cloud',
},
});
delete process.env.PDF_MINERU_BASE_URL;
delete process.env.PDF_MINERU_API_KEY;
});
it('returns 400 for unsupported course material MIME types', async () => {
const res = await postExtractDocument({
file: new File(['x,y'], 'sheet.csv', { type: 'text/csv' }),
});
const json = await res.json();
expect(res.status).toBe(400);
expect(json).toMatchObject({
success: false,
errorCode: 'INVALID_REQUEST',
});
});
it('rejects server-role unmanaged media extraction before caller AK/SK or base URL reaches the adapter', async () => {
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'server');
const { ServerManagedProviderRequiredError } =
await import('@/lib/server/provider-access-policy');
mocks.requireProviderAccess.mockImplementation(() => {
throw new ServerManagedProviderRequiredError();
});
const res = await postExtractDocument({
file: new File(['audio'], 'lesson.mp3', { type: 'audio/mpeg' }),
providerId: 'alidocmind',
baseUrl: 'https://attacker.invalid',
accessKeyId: 'caller-ak',
accessKeySecret: 'caller-sk',
});
const json = await res.json();
expect(res.status).toBe(403);
expect(json).toMatchObject({ success: false, errorCode: 'FORBIDDEN' });
expect(mocks.requireProviderAccess).toHaveBeenCalledWith('pdf', 'alidocmind');
expect(mocks.extractMedia).not.toHaveBeenCalled();
});
it('returns 413 before extraction when the file exceeds the per-file size limit', async () => {
const res = await postExtractDocument({
file: new File([new Uint8Array(51 * 1024 * 1024)], 'large.pdf', {
type: 'application/pdf',
}),
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
});
const json = await res.json();
expect(res.status).toBe(413);
expect(json).toMatchObject({
success: false,
errorCode: 'INVALID_REQUEST',
});
expect(json.error).toContain('Maximum size is 50MB');
expect(mocks.parseWithMinerUCloud).not.toHaveBeenCalled();
});
it('returns 400 for an unknown requested provider', async () => {
const res = await postExtractDocument({
file: new File(['hello'], 'notes.txt', { type: 'text/plain' }),
providerId: 'missing-provider',
});
const json = await res.json();
expect(res.status).toBe(400);
expect(json).toMatchObject({
success: false,
errorCode: 'INVALID_REQUEST',
error: 'Unknown document extractor provider: missing-provider',
});
});
it('treats an incompatible preferred provider as a hint and falls back by MIME type', async () => {
const res = await postExtractDocument({
file: new File(['hello'], 'notes.txt', { type: 'text/plain' }),
providerId: 'unpdf',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json).toMatchObject({
success: true,
data: {
text: 'hello',
metadata: {
mimeType: 'text/plain',
parser: 'plain-text',
},
},
});
});
it('returns actionable 422 diagnostics when DOCX requires unconfigured MinerU', async () => {
const res = await postExtractDocument({
file: new File(['not really docx'], 'lesson.docx', {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}),
});
const json = await res.json();
expect(res.status).toBe(422);
expect(json).toMatchObject({
success: false,
errorCode: 'INVALID_REQUEST',
});
expect(json.error).toContain('DOCX extraction requires a configured MinerU document extractor');
expect(json.error).toContain('self-hosted MinerU base URL or a MinerU Cloud API key');
});
it('allows MinerU Cloud PDF extraction with an API key and no base URL', async () => {
const res = await postExtractDocument({
file: new File(['%PDF-1.4'], 'lesson.pdf', { type: 'application/pdf' }),
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json).toMatchObject({
success: true,
data: {
text: 'cloud parsed text',
metadata: {
parser: 'mineru-cloud',
},
},
});
expect(mocks.parseWithMinerUCloud).toHaveBeenCalledWith(
expect.objectContaining({
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: undefined,
}),
expect.any(Buffer),
'lesson.pdf',
);
});
it('falls back to MinerU Cloud for DOCX when self-hosted MinerU is unavailable and a cloud key is provided', async () => {
const res = await postExtractDocument({
file: new File(['not really docx'], 'lesson.docx', {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}),
apiKey: 'cloud-key',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json).toMatchObject({
success: true,
data: {
text: 'cloud parsed text',
metadata: {
parser: 'mineru-cloud',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
},
},
});
expect(mocks.parseWithMinerUCloud).toHaveBeenCalledWith(
expect.objectContaining({
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: undefined,
}),
expect.any(Buffer),
'lesson.docx',
);
});
});