199 lines
5.9 KiB
TypeScript
199 lines
5.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
isServerConfiguredProvider: vi.fn(() => false),
|
|
resolvePDFApiKey: vi.fn((_providerId: string, clientKey?: string) => clientKey || ''),
|
|
resolvePDFBaseUrl: vi.fn((_providerId: string, clientBaseUrl?: string) => clientBaseUrl),
|
|
parseWithMinerUCloud: 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,
|
|
resolvePDFApiKey: mocks.resolvePDFApiKey,
|
|
resolvePDFBaseUrl: mocks.resolvePDFBaseUrl,
|
|
}));
|
|
|
|
vi.mock('@/lib/pdf/mineru-cloud', () => ({
|
|
parseWithMinerUCloud: mocks.parseWithMinerUCloud,
|
|
}));
|
|
|
|
async function postExtractDocument(input: {
|
|
file: File;
|
|
providerId?: string;
|
|
apiKey?: string;
|
|
baseUrl?: 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);
|
|
|
|
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.resolvePDFApiKey.mockImplementation(
|
|
(_providerId: string, clientKey?: string) => clientKey || '',
|
|
);
|
|
mocks.resolvePDFBaseUrl.mockImplementation(
|
|
(_providerId: string, clientBaseUrl?: string) => clientBaseUrl,
|
|
);
|
|
mocks.parseWithMinerUCloud.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('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',
|
|
);
|
|
});
|
|
});
|