117 lines
4.0 KiB
TypeScript
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();
|
|
});
|
|
});
|