import { afterEach, describe, expect, it, vi } from 'vitest'; import { deserialize } from 'bson'; import sharp from 'sharp'; import trace from './trace.js'; afterEach(() => { vi.unstubAllGlobals(); }); /** Build a small PNG with an opaque circle in the center on a transparent bg. */ async function makePng(size = 20): Promise { const rgba = Buffer.alloc(size * size * 4); const cx = size / 2; const cy = size / 2; const r = size / 3; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { if (Math.hypot(x - cx, y - cy) <= r) { const i = (y * size + x) * 4; rgba[i] = 255; rgba[i + 1] = 255; rgba[i + 2] = 255; rgba[i + 3] = 255; } } } return sharp(rgba, { raw: { width: size, height: size, channels: 4 } }) .png() .toBuffer(); } function stubFetch(body: Buffer) { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( new Response(new Uint8Array(body), { headers: { 'content-type': 'image/png' }, }), ), ); } describe('trace route', () => { it('rejects a missing url', async () => { const res = await trace.request('/'); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: 'Required' }); }); it('rejects a non-http url', async () => { const res = await trace.request('/?url=file%3A%2F%2F%2Fetc%2Fpasswd'); expect(res.status).toBe(400); }); it('returns 502 when the upstream fetch fails', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(new Response('error', { status: 500 })), ); const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png'); expect(res.status).toBe(502); }); it('returns a BSON shape by default', async () => { stubFetch(await makePng()); const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png'); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toBe('application/octet-stream'); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.format).toBe('shape'); expect(result.mode).toBe('alpha'); expect(result.width).toBe(20); expect(result.height).toBe(20); expect(typeof result.svg).toBe('string'); expect(result.svg).toContain(' { stubFetch(await makePng()); const res = await trace.request( '/?url=https%3A%2F%2Fexample.com%2Fa.png&format=svg', ); expect(res.status).toBe(200); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.format).toBe('svg'); expect(result.shape).toBeUndefined(); expect(typeof result.svg).toBe('string'); }); it('supports bw mode', async () => { stubFetch(await makePng()); const res = await trace.request( '/?url=https%3A%2F%2Fexample.com%2Fa.png&mode=bw', ); expect(res.status).toBe(200); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.mode).toBe('bw'); expect(result.shape.outline.length).toBeGreaterThan(3); }); it('applies an inset offset to the shape', async () => { stubFetch(await makePng()); const res = await trace.request( '/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=-2', ); expect(res.status).toBe(200); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.offset).toBe(-2); expect(result.shape.outline.length).toBeGreaterThan(3); // An inset of 2px on a 20px circle must be visibly smaller. const extent = (pts: number[][]) => [ Math.min(...pts.map((p) => p[0]!)), Math.max(...pts.map((p) => p[0]!)), ]; const [minX, maxX] = extent(result.shape.outline); expect((maxX ?? 0) - (minX ?? 0)).toBeLessThan(20); }); it('applies an outset offset to the shape', async () => { stubFetch(await makePng()); const res = await trace.request( '/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=2', ); expect(res.status).toBe(200); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.offset).toBe(2); expect(result.shape.outline.length).toBeGreaterThan(3); }); it('rejects a non-numeric offset', async () => { stubFetch(await makePng()); const res = await trace.request( '/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=abc', ); expect(res.status).toBe(400); }); it('omits offset when not requested', async () => { stubFetch(await makePng()); const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png'); const result = deserialize(new Uint8Array(await res.arrayBuffer())); expect(result.offset).toBeUndefined(); }); });