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

323 lines
10 KiB
TypeScript

import JSZip from 'jszip';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { parseWithMinerUCloud } from '@/lib/pdf/mineru-cloud';
const mocks = vi.hoisted(() => ({
fetchPinnedPublicUrl: vi.fn(),
}));
vi.mock('@/lib/server/public-url-fetch', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/server/public-url-fetch')>();
return { ...actual, fetchPinnedPublicUrl: mocks.fetchPinnedPublicUrl };
});
vi.mock('@/lib/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}));
describe('MinerU Cloud document upload', () => {
beforeEach(() => {
mocks.fetchPinnedPublicUrl.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('preserves supported Office filename extensions for Cloud type inference', async () => {
const zip = new JSZip();
zip.file('full.md', '# Parsed lesson');
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' });
const batchBodies: unknown[] = [];
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
if (url.endsWith('/file-urls/batch')) {
batchBodies.push(JSON.parse(String(init?.body)));
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
batch_id: 'batch-1',
file_urls: ['https://upload.example/lesson.docx'],
},
}),
{ status: 200 },
);
}
if (url === 'https://upload.example/lesson.docx') {
return new Response('', { status: 200 });
}
if (url.endsWith('/extract-results/batch/batch-1')) {
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
extract_result: {
file_name: 'lesson.docx',
state: 'done',
full_zip_url: 'https://download.example/result.zip',
},
},
}),
{ status: 200 },
);
}
if (url === 'https://download.example/result.zip') {
return new Response(
zipBuffer.buffer.slice(
zipBuffer.byteOffset,
zipBuffer.byteOffset + zipBuffer.byteLength,
) as ArrayBuffer,
{ status: 200 },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const disposals: Array<ReturnType<typeof vi.fn>> = [];
mocks.fetchPinnedPublicUrl.mockImplementation(async (input, init) => {
const dispose = vi.fn().mockResolvedValue(undefined);
disposals.push(dispose);
return {
response: await fetchMock(input, init),
dispose,
};
});
const result = await parseWithMinerUCloud(
{
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: 'https://mineru.example/api/v4',
},
Buffer.from('docx bytes'),
'lesson.docx',
);
expect(result.text).toContain('Parsed lesson');
expect(result.metadata?.parser).toBe('mineru-cloud');
expect(batchBodies).toEqual([
expect.objectContaining({
files: [{ name: 'lesson.docx' }],
}),
]);
expect(mocks.fetchPinnedPublicUrl).toHaveBeenNthCalledWith(
1,
'https://upload.example/lesson.docx',
expect.objectContaining({
method: 'PUT',
body: Buffer.from('docx bytes'),
maxRequestBytes: 50 * 1024 * 1024,
maxResponseBytes: 1024 * 1024,
}),
);
expect(mocks.fetchPinnedPublicUrl).toHaveBeenNthCalledWith(
2,
'https://download.example/result.zip',
expect.objectContaining({
method: 'GET',
maxResponseBytes: 100 * 1024 * 1024,
}),
);
expect(disposals).toHaveLength(2);
for (const dispose of disposals) expect(dispose).toHaveBeenCalledOnce();
});
it('preserves legacy Office filenames (cloud accepts .doc/.ppt/.xls)', async () => {
const zip = new JSZip();
zip.file('full.md', '# Parsed legacy lesson');
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' });
const batchBodies: unknown[] = [];
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
if (url.endsWith('/file-urls/batch')) {
batchBodies.push(JSON.parse(String(init?.body)));
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
batch_id: 'batch-legacy',
file_urls: ['https://upload.example/legacy.doc'],
},
}),
{ status: 200 },
);
}
if (url === 'https://upload.example/legacy.doc') {
return new Response('', { status: 200 });
}
if (url.endsWith('/extract-results/batch/batch-legacy')) {
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
extract_result: {
file_name: 'legacy.doc',
state: 'done',
full_zip_url: 'https://download.example/legacy.zip',
},
},
}),
{ status: 200 },
);
}
if (url === 'https://download.example/legacy.zip') {
return new Response(
zipBuffer.buffer.slice(
zipBuffer.byteOffset,
zipBuffer.byteOffset + zipBuffer.byteLength,
) as ArrayBuffer,
{ status: 200 },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
mocks.fetchPinnedPublicUrl.mockImplementation(async (input, init) => ({
response: await fetchMock(input, init),
dispose: vi.fn().mockResolvedValue(undefined),
}));
const result = await parseWithMinerUCloud(
{
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: 'https://mineru.example/api/v4',
},
Buffer.from('doc bytes'),
'legacy.doc',
);
expect(result.text).toContain('Parsed legacy lesson');
expect(batchBodies).toEqual([
expect.objectContaining({
files: [{ name: 'legacy.doc' }],
}),
]);
});
it('fails closed on a presigned upload redirect without polling or forwarding API auth', async () => {
const fetchMock = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/file-urls/batch')) {
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
batch_id: 'batch-redirect',
file_urls: ['https://upload.example/presigned?signature=secret'],
},
}),
{ status: 200 },
);
}
throw new Error(`Unexpected direct fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const dispose = vi.fn().mockResolvedValue(undefined);
mocks.fetchPinnedPublicUrl.mockResolvedValue({
response: new Response(null, {
status: 307,
headers: { Location: 'http://127.0.0.1/internal' },
}),
dispose,
});
await expect(
parseWithMinerUCloud(
{
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: 'https://mineru.example/api/v4',
},
Buffer.from('document bytes'),
'lesson.pdf',
),
).rejects.toThrow('MinerU Cloud upload redirect is not allowed');
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledOnce();
expect(mocks.fetchPinnedPublicUrl.mock.calls[0][1]).not.toHaveProperty('headers');
expect(fetchMock).toHaveBeenCalledOnce();
expect(dispose).toHaveBeenCalledOnce();
});
it('fails closed on a result ZIP redirect and disposes both pinned connections', async () => {
const fetchMock = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/file-urls/batch')) {
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
batch_id: 'batch-zip-redirect',
file_urls: ['https://upload.example/presigned?signature=secret'],
},
}),
{ status: 200 },
);
}
if (url.endsWith('/extract-results/batch/batch-zip-redirect')) {
return new Response(
JSON.stringify({
code: 0,
msg: 'ok',
data: {
extract_result: {
file_name: 'lesson.pdf',
state: 'done',
full_zip_url: 'https://download.example/result.zip?signature=secret',
},
},
}),
{ status: 200 },
);
}
throw new Error(`Unexpected direct fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const uploadDispose = vi.fn().mockResolvedValue(undefined);
const zipDispose = vi.fn().mockResolvedValue(undefined);
mocks.fetchPinnedPublicUrl
.mockResolvedValueOnce({
response: new Response(null, { status: 200 }),
dispose: uploadDispose,
})
.mockResolvedValueOnce({
response: new Response(null, {
status: 302,
headers: { Location: 'http://100.100.100.200/latest/meta-data' },
}),
dispose: zipDispose,
});
await expect(
parseWithMinerUCloud(
{
providerId: 'mineru-cloud',
apiKey: 'cloud-key',
baseUrl: 'https://mineru.example/api/v4',
},
Buffer.from('document bytes'),
'lesson.pdf',
),
).rejects.toThrow('MinerU Cloud ZIP download redirect is not allowed');
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledTimes(2);
expect(mocks.fetchPinnedPublicUrl.mock.calls[0][1]).not.toHaveProperty('headers');
expect(mocks.fetchPinnedPublicUrl.mock.calls[1][1]).not.toHaveProperty('headers');
expect(uploadDispose).toHaveBeenCalledOnce();
expect(zipDispose).toHaveBeenCalledOnce();
});
});