import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { NextRequest } from 'next/server'; import { GET } from '../route'; /* ── GET /ipfs/ ── * * Audit: `detectContentType(cid)` gaf onafhankelijk van de input altijd * `application/octet-stream` terug, terwijl de commentaar claimde dat Kubo het * echte type bepaalt. Content die als octet-stream geserved wordt, kan de * browser niet als image/audio/video of tekst tonen. * * Deze tests dwingen af dat de response het content-type van de upstream * (`cat`) doorgeeft, met `application/octet-stream` als fallback. */ const CID = 'QmTestCID000'; function makeReq(query = ''): NextRequest { return new NextRequest(`http://localhost/ipfs/${CID}${query}`); } function call(query = '') { return GET(makeReq(query), { params: Promise.resolve({ cid: CID }) }); } /** Upstream-stand-in: `cat` levert bytes + een Content-Type. */ function kuboReturning(contentType: string | null, body = 'hallo wereld'): Response { const headers: Record = {}; if (contentType !== null) headers['Content-Type'] = contentType; // Uint8Array-body zodat Response zelf geen Content-Type zet. const bytes = new TextEncoder().encode(body); return new Response(bytes, { status: 200, headers }); } const kuboFetch = vi.fn(async (_url: string, _init: RequestInit) => kuboReturning('text/plain; charset=utf-8'), ); beforeEach(() => { vi.clearAllMocks(); process.env.KUBO_API_URL = 'http://kubo.test:5001'; vi.stubGlobal('fetch', kuboFetch); }); afterEach(() => { vi.unstubAllGlobals(); delete process.env.KUBO_API_URL; }); describe('content-type van de gateway', () => { it('geeft het content-type van de upstream door', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning('text/html; charset=utf-8')); const res = await call(); expect(res.status).toBe(200); expect(res.headers.get('Content-Type')).toBe('text/html; charset=utf-8'); }); it('geeft text/plain door, inclusief charset', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning('text/plain; charset=utf-8')); const res = await call(); expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8'); }); it('geef application/json door zonder charset-params', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning('application/json')); const res = await call(); expect(res.headers.get('Content-Type')).toBe('application/json'); }); it('valt terug op application/octet-stream als upstream geen type stuurt', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning(null)); const res = await call(); expect(res.status).toBe(200); expect(res.headers.get('Content-Type')).toBe('application/octet-stream'); }); it('valt terug op application/octet-stream bij een lege content-type', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning('')); const res = await call(); expect(res.headers.get('Content-Type')).toBe('application/octet-stream'); }); it('valt terug op application/octet-stream bij een ongeldige content-type', async () => { kuboFetch.mockResolvedValueOnce(kuboReturning('geen-media-type')); const res = await call(); expect(res.status).toBe(200); expect(res.headers.get('Content-Type')).toBe('application/octet-stream'); }); it('raakt Kubo via POST /api/v0/cat met de CID als arg', async () => { await call(); expect(kuboFetch).toHaveBeenCalledTimes(1); const [url, init] = kuboFetch.mock.calls[0]; expect(url).toBe(`http://kubo.test:5001/api/v0/cat?arg=${CID}`); expect(init.method).toBe('POST'); }); it('geeft de body byte-voor-byte door', async () => { const payload = '{"hallo":"wereld"}'; kuboFetch.mockResolvedValueOnce(kuboReturning('application/json', payload)); const res = await call(); expect(await res.text()).toBe(payload); }); }); describe('overige gateway-headers', () => { it('zet Content-Disposition inline zonder filename', async () => { const res = await call(); expect(res.headers.get('Content-Disposition')).toBe('inline'); }); it('zet Content-Disposition met de gevraagde filename', async () => { const res = await call('?filename=rap.txt'); expect(res.headers.get('Content-Disposition')).toBe('inline; filename="rap.txt"'); }); it('sluit het response aan op immutable caching', async () => { const res = await call(); expect(res.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable'); }); }); describe('foutpaden blijven ongewijzigd', () => { it('weigert een ongeldig CID formaat zonder Kubo te raken', async () => { const res = await GET(new NextRequest('http://localhost/ipfs/niet-een-cid'), { params: Promise.resolve({ cid: 'niet-een-cid' }), }); expect(res.status).toBe(400); expect(kuboFetch).not.toHaveBeenCalled(); }); it('geeft een Kubo-foutstatus door', async () => { kuboFetch.mockResolvedValueOnce( new Response('not found', { status: 404, headers: { 'Content-Type': 'text/plain' } }), ); const res = await call(); expect(res.status).toBe(404); }); it('geeft 501 zonder KUBO_API_URL', async () => { delete process.env.KUBO_API_URL; const res = await call(); expect(res.status).toBe(501); expect(kuboFetch).not.toHaveBeenCalled(); }); it('geeft 502 wanneer de fetch faalt', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => { throw new Error('connect ECONNREFUSED'); }), ); vi.spyOn(console, 'error').mockImplementation(() => {}); const res = await call(); expect(res.status).toBe(502); }); });