fa7de62707
- detectContentType(cid) ignoreerde zijn input en gaf altijd octet-stream terug, terwijl de commentaar claimde dat Kubo het echte type bepaalt - De response draagt nu het Content-Type dat Kubo cat terugstuurt - resolveContentType() valt terug op application/octet-stream bij ontbrekend, leeg of ongeldig type; het patroon weigert CR/LF en quotes zodat upstream- headers niet in de response gespiegeld kunnen worden - Streaming ongewijzigd (body nog volledig in geheugen), headers idem - Nieuwe route-tests: content-type-passthrough, fallbackgevallen, Kubo-aanroep, body, overige headers en de ongewijzigde foutpaden (400/404/501/502)
181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
import { GET } from '../route';
|
|
|
|
/* ── GET /ipfs/<cid> ──
|
|
*
|
|
* 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<string, string> = {};
|
|
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);
|
|
});
|
|
});
|