Files
openmaic/OpenMAIC/tests/document/alidocmind-image-fetch.test.ts
2026-08-16 14:58:47 +08:00

117 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
fetchPinnedPublicUrl: vi.fn(),
}));
vi.mock('@/lib/logger', () => ({
createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
}));
vi.mock('@/lib/server/public-url-fetch', () => ({
fetchPinnedPublicUrl: mocks.fetchPinnedPublicUrl,
}));
// Keep image conversion deterministic; these tests exercise network policy.
vi.mock('sharp', () => ({
default: () => ({ png: () => ({ toBuffer: async () => Buffer.from([9]) }) }),
}));
import { fetchAliDocMindImageAsBase64 } from '@/lib/pdf/pdf-providers';
const OSS = 'https://bkt.oss-cn-hangzhou.aliyuncs.com/img.png?sig=x';
function pinned(response: Response) {
const dispose = vi.fn().mockResolvedValue(undefined);
mocks.fetchPinnedPublicUrl.mockResolvedValue({ response, dispose });
return { dispose };
}
/** Build a Response whose body streams `chunks` and omits Content-Length. */
function streamingResponse(chunks: Uint8Array[]): Response {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
for (const c of chunks) controller.enqueue(c);
controller.close();
},
});
// No content-length header — forces the cumulative streaming check.
return new Response(stream, { status: 200, headers: {} });
}
describe('fetchAliDocMindImageAsBase64', () => {
beforeEach(() => {
mocks.fetchPinnedPublicUrl.mockReset();
});
it('rejects a non-OSS host without fetching (SSRF allowlist)', async () => {
const result = await fetchAliDocMindImageAsBase64('https://169.254.169.254/latest/meta-data');
expect(result).toBeNull();
expect(mocks.fetchPinnedPublicUrl).not.toHaveBeenCalled();
});
it('downloads a trusted OSS image only through the DNS-pinned public helper', async () => {
const { dispose } = pinned(new Response(new Uint8Array([1, 2, 3]), { status: 200 }));
const result = await fetchAliDocMindImageAsBase64(OSS);
expect(result).toBe('data:image/png;base64,CQ==');
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledOnce();
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(OSS, {
signal: expect.any(AbortSignal),
maxResponseBytes: 10 * 1024 * 1024,
});
expect(dispose).toHaveBeenCalledOnce();
});
it('upgrades a legacy HTTP OSS URL before opening the pinned connection', async () => {
pinned(new Response(new Uint8Array([1, 2, 3]), { status: 200 }));
await fetchAliDocMindImageAsBase64('http://bkt.oss-cn-hangzhou.aliyuncs.com/img.png?sig=x');
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(OSS, {
signal: expect.any(AbortSignal),
maxResponseBytes: 10 * 1024 * 1024,
});
});
it('fails closed on an OSS redirect without following its Location', async () => {
const { dispose } = pinned(
new Response(null, {
status: 302,
headers: { location: 'http://169.254.169.254/latest/meta-data' },
}),
);
const result = await fetchAliDocMindImageAsBase64(OSS);
expect(result).toBeNull();
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledOnce();
expect(dispose).toHaveBeenCalledOnce();
});
it('aborts and rejects an oversized stream with no Content-Length', async () => {
// 12 x 1MiB chunks = 12 MiB > 10 MiB cap, streamed with no content-length.
const oneMiB = new Uint8Array(1024 * 1024);
const chunks = Array.from({ length: 12 }, () => oneMiB);
const { dispose } = pinned(streamingResponse(chunks));
const result = await fetchAliDocMindImageAsBase64(OSS);
expect(result).toBeNull(); // aborted before buffering the whole body
expect(dispose).toHaveBeenCalledOnce();
});
it('rejects a declared oversized Content-Length up front', async () => {
const { dispose } = pinned(
new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: { 'content-length': String(20 * 1024 * 1024) },
}),
);
const result = await fetchAliDocMindImageAsBase64(OSS);
expect(result).toBeNull();
expect(dispose).toHaveBeenCalledOnce();
});
});