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({ 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(); }); });