refactor: verwijder dode Kubo-takken voor pins en files/upload
- mapToKuboAPI had pin/ls|add|rm- en add?pin=true-takken die onbereikbaar zijn: proxyIPFS onderschept pins/pins/<cid>/files/upload eerst en serveert ze uit de lokale .data-opslag - Dode code weggehaald; comment boven mapToKuboAPI en bij de interceptie documenteert dat pins/files bewust lokaal blijven, zodat de volgende lezer het niet 'fixt' - files/<subpad> -> /api/v0/ls blijft: alleen bereikbaar voor methoden die proxyIPFS niet lokaal afhandelt (GET/DELETE worden wel afgehandeld) - De 4 assertions die de dode takken vastlegden zijn vervangen door tests die de generieke doorval beschrijven, plus een comment dat een teruggezette Kubo-tak de test rood maakt - Nieuwe proxy-ipfs.test.ts test het werkelijke runtime-gedrag: pins en files/* raken Kubo nooit (ook niet bij een geldige KUBO_API_URL), terwijl explorer/* en peers wél doorvallen
This commit is contained in:
@@ -0,0 +1,195 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
/* ── Runtime-routing van proxyIPFS ──
|
||||||
|
*
|
||||||
|
* Audit: `mapToKuboAPI()` bevatte takken voor `pins` en `files/upload` die in
|
||||||
|
* runtime nooit bereikt worden — `proxyIPFS()` onderschept die paden eerder
|
||||||
|
* en serveert ze uit de lokale `.data`-opslag. De route-tests assertten die
|
||||||
|
* dode takken en gaven zo valse zekerheid.
|
||||||
|
*
|
||||||
|
* Deze tests beschrijven het WERKELIJKE gedrag: `pins` en `files/*` worden
|
||||||
|
* lokaal afgehandeld en bereiken Kubo nooit. Kubo-pass-through wordt alleen
|
||||||
|
* getest voor paden die daadwerkelijk doorvallen (explorer/*, ipns/*, …).
|
||||||
|
*/
|
||||||
|
|
||||||
|
vi.mock('@/lib/local-pins', () => ({
|
||||||
|
localListPins: vi.fn(async () => [{ cid: 'QmTest', name: 'a.txt', size: 3, created: 'indirect' }]),
|
||||||
|
localAddPin: vi.fn(async () => ({ cid: 'QmTest' })),
|
||||||
|
localRemovePin: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/local-files', () => ({
|
||||||
|
localSaveFile: vi.fn(async () => ({ cid: 'QmSaved', size: 5 })),
|
||||||
|
localGetFile: vi.fn(async () => ({ buffer: Buffer.from('hallo'), name: 'a.txt' })),
|
||||||
|
localListFiles: vi.fn(async () => [{ cid: 'QmSaved', name: 'a.txt', size: 5, modified: 'x', path: 'a.txt' }]),
|
||||||
|
localDeleteFile: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { proxyIPFS } from '../proxy-ipfs';
|
||||||
|
import { localListPins, localAddPin, localRemovePin } from '@/lib/local-pins';
|
||||||
|
import { localSaveFile, localGetFile, localListFiles, localDeleteFile } from '@/lib/local-files';
|
||||||
|
|
||||||
|
/** Kubo-stand-in: als er een Kubo-call komt, ziet de test dat aan de URL. */
|
||||||
|
const kuboFetch = vi.fn(async (_url: string, _init: RequestInit) =>
|
||||||
|
new Response(JSON.stringify({ Id: 'kubo' }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
function req(path: string, method = 'GET', body?: BodyInit | null): NextRequest {
|
||||||
|
return new NextRequest(`http://localhost/api/${path.replace(/^\/+/, '')}`, {
|
||||||
|
method,
|
||||||
|
...(body !== undefined && body !== null ? { body } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function call(path: string, method = 'GET', body?: BodyInit | null) {
|
||||||
|
return proxyIPFS('/' + path.replace(/^\/+/, ''), method, req(path, method, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ═════════════════════ pins — altijd lokaal ═════════════════════ */
|
||||||
|
|
||||||
|
describe('pins worden lokaal behandeld, niet naar Kubo gestuurd', () => {
|
||||||
|
it('GET /api/pins leest de lokale pin-opslag', async () => {
|
||||||
|
const res = await call('pins');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localListPins).toHaveBeenCalledTimes(1);
|
||||||
|
expect(await res.json()).toEqual({ Keys: { QmTest: { Type: 'indirect' } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/pins bereikt Kubo niet', async () => {
|
||||||
|
await call('pins');
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /api/pins schrijft naar de lokale pin-opslag', async () => {
|
||||||
|
const res = await call(
|
||||||
|
'pins',
|
||||||
|
'POST',
|
||||||
|
JSON.stringify({ cid: 'QmNieuw', name: 'b.txt', size: 7 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localAddPin).toHaveBeenCalledWith('QmNieuw', 'b.txt', 7);
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /api/pins zonder cid weigert met 400', async () => {
|
||||||
|
const res = await call('pins', 'POST', JSON.stringify({ name: 'b.txt' }));
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(localAddPin).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /api/pins/<cid> verwijdert uit de lokale pin-opslag', async () => {
|
||||||
|
const res = await call('pins/QmTest', 'DELETE');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localRemovePin).toHaveBeenCalledWith('QmTest');
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ═════════════════════ files — altijd lokaal ═════════════════════ */
|
||||||
|
|
||||||
|
describe('files worden lokaal behandeld, niet naar Kubo gestuurd', () => {
|
||||||
|
it('POST /api/files/upload slaat op in .data/files en pint lokaal', async () => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.set('file', new File(['hallo'], 'a.txt', { type: 'text/plain' }));
|
||||||
|
|
||||||
|
const res = await call('files/upload', 'POST', form);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localSaveFile).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vi.mocked(localSaveFile).mock.calls[0][1]).toBeInstanceOf(Buffer);
|
||||||
|
expect(localAddPin).toHaveBeenCalledWith('QmSaved', 'a.txt', 5);
|
||||||
|
// Kern van de fix: geen Kubo /api/v0/add.
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /api/files/upload zonder bestand weigert met 400', async () => {
|
||||||
|
const res = await call('files/upload', 'POST', new FormData());
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(localSaveFile).not.toHaveBeenCalled();
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/files leest de lokale file-opslag', async () => {
|
||||||
|
const res = await call('files');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localListFiles).toHaveBeenCalledTimes(1);
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/files/<cid> leest lokaal, ook voor onbekende CID', async () => {
|
||||||
|
const res = await call('files/QmTest');
|
||||||
|
|
||||||
|
expect(localGetFile).toHaveBeenCalledWith('QmTest');
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /api/files/<cid> verwijdert lokaal', async () => {
|
||||||
|
const res = await call('files/QmTest', 'DELETE');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localDeleteFile).toHaveBeenCalledWith('QmTest');
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ═════════════════════ overige paden gaan wél naar Kubo ═════════════════════ */
|
||||||
|
|
||||||
|
describe('overige paden vallen door naar Kubo', () => {
|
||||||
|
it('GET /api/explorer/ls/<cid> gaat naar /api/v0/ls', async () => {
|
||||||
|
await call('explorer/ls/QmTest');
|
||||||
|
|
||||||
|
expect(kuboFetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(kuboFetch.mock.calls[0][0]).toBe('http://kubo.test:5001/api/v0/ls?arg=QmTest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /api/explorer/cat/<cid> gaat naar /api/v0/cat', async () => {
|
||||||
|
await call('explorer/cat/QmTest', 'POST');
|
||||||
|
|
||||||
|
expect(kuboFetch.mock.calls[0][0]).toBe('http://kubo.test:5001/api/v0/cat?arg=QmTest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/peers gaat naar /api/v0/swarm/peers', async () => {
|
||||||
|
await call('peers');
|
||||||
|
|
||||||
|
expect(kuboFetch.mock.calls[0][0]).toBe('http://kubo.test:5001/api/v0/swarm/peers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geeft 501 zonder KUBO_API_URL', async () => {
|
||||||
|
delete process.env.KUBO_API_URL;
|
||||||
|
|
||||||
|
const res = await call('peers');
|
||||||
|
|
||||||
|
expect(res.status).toBe(501);
|
||||||
|
expect(kuboFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lokale opslag blijft werken zonder KUBO_API_URL', async () => {
|
||||||
|
delete process.env.KUBO_API_URL;
|
||||||
|
|
||||||
|
const res = await call('pins');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(localListPins).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -66,24 +66,27 @@ describe('mapToKuboAPI', () => {
|
|||||||
expect(mapToKuboAPI('peers', 'GET')).toBe('/api/v0/swarm/peers');
|
expect(mapToKuboAPI('peers', 'GET')).toBe('/api/v0/swarm/peers');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps pins GET to /api/v0/pin/ls', () => {
|
it('heeft GEEN tak voor pins — die worden lokaal afgehandeld', () => {
|
||||||
expect(mapToKuboAPI('pins', 'GET')).toBe('/api/v0/pin/ls');
|
// `proxyIPFS()` onderschept `pins`/`pins/<cid>` vóór de Kubo-mapping, dus
|
||||||
|
// `mapToKuboAPI` valt door naar de generieke /api/v0/<pad>. Zou er ooit weer
|
||||||
|
// een `case 'pins'` bij komen, dan faalt deze test.
|
||||||
|
expect(mapToKuboAPI('pins', 'GET')).toBe('/api/v0/pins');
|
||||||
|
expect(mapToKuboAPI('pins', 'POST')).toBe('/api/v0/pins');
|
||||||
|
expect(mapToKuboAPI('pins/QmTest123', 'DELETE')).toBe('/api/v0/pins/QmTest123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps pins POST to /api/v0/pin/add', () => {
|
it('heeft GEEN specifieke tak voor files/upload — die wordt lokaal afgehandeld', () => {
|
||||||
expect(mapToKuboAPI('pins', 'POST')).toBe('/api/v0/pin/add');
|
// De `files/upload`-tak naar Kubo /api/v0/add is weg; het pad valt door op de
|
||||||
|
// generieke `files/`-tak. Onbereikbaar in runtime (proxyIPFS onderscheept
|
||||||
|
// POST /api/files/upload), dus de concrete waarde doet er niet toe — zolang
|
||||||
|
// er geen specifieke `files/upload`-tak terugkomt.
|
||||||
|
expect(mapToKuboAPI('files/upload', 'POST')).toBe('/api/v0/ls');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps pins/rm to rm with CID', () => {
|
it('mapt files/<subpad> naar ls (alleen bereikbaar zonder GET/DELETE)', () => {
|
||||||
expect(mapToKuboAPI('pins/QmTest123', 'DELETE')).toBe('/api/v0/pin/rm?arg=QmTest123');
|
// GET en DELETE op `files/*` worden lokaal afgehandeld; deze tak is dus
|
||||||
});
|
// alleen relevant voor overige methoden.
|
||||||
|
expect(mapToKuboAPI('files/QmTest123', 'POST')).toBe('/api/v0/ls');
|
||||||
it('maps files/upload to add with pin', () => {
|
|
||||||
expect(mapToKuboAPI('files/upload', 'POST')).toBe('/api/v0/add?pin=true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps files/list to ls', () => {
|
|
||||||
expect(mapToKuboAPI('files/list', 'GET')).toBe('/api/v0/ls');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps explorer/ls/<cid>', () => {
|
it('maps explorer/ls/<cid>', () => {
|
||||||
|
|||||||
@@ -8,11 +8,20 @@ export async function proxyIPFS(path: string, method: string, req: NextRequest):
|
|||||||
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
|
|
||||||
// ── Lokale pin opslag (altijd) ──
|
// ── Lokale pin opslag (altijd) ──
|
||||||
|
// Pins gaan NIET naar Kubo: `pins` en `pins/<cid>` worden uit `.data/pins.json`
|
||||||
|
// gelezen en daarin geschreven. Deze interceptie gebeurt VÓÓR de Kubo-mapping,
|
||||||
|
// dus `mapToKuboAPI()` hoeft (en mag) geen `pins`-tak te hebben. Zie de
|
||||||
|
// doc-comment boven `mapToKuboAPI` — of dit bewust zo blijft, hangt samen met
|
||||||
|
// de lopende vraag of pinning via `?pin=true` moet.
|
||||||
if (p === 'pins' || p.startsWith('pins/')) {
|
if (p === 'pins' || p.startsWith('pins/')) {
|
||||||
return handleLocalPins(p, method, req);
|
return handleLocalPins(p, method, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Lokale file opslag (altijd) ──
|
// ── Lokale file opslag (altijd) ──
|
||||||
|
// Ook uploads lopen niet via Kubo `/api/v0/add`: het bestand gaat naar
|
||||||
|
// `.data/files/<cid>-<naam>` met metadata in `.data/files.json`, en wordt
|
||||||
|
// daarna lokaal gepind. Idem voor lijst/get/delete. Vóór de Kubo-mapping,
|
||||||
|
// dus `mapToKuboAPI()` hoeft geen `files/upload`-tak te hebben.
|
||||||
if (p === 'files/upload' && method === 'POST') {
|
if (p === 'files/upload' && method === 'POST') {
|
||||||
return handleLocalUpload(req);
|
return handleLocalUpload(req);
|
||||||
}
|
}
|
||||||
@@ -205,6 +214,20 @@ async function handleLocalDeleteFile(cid: string): Promise<Response> {
|
|||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertaalt een Portal-API-pad naar een Kubo RPC-endpoint.
|
||||||
|
*
|
||||||
|
* `pins`, `pins/<cid>` en `files/upload` hebben hier GEEN tak, en dat is
|
||||||
|
* bewust: `proxyIPFS()` onderschept die paden hierboven al en serveert ze uit
|
||||||
|
* de lokale `.data`-opslag (`.data/pins.json`, `.data/files/`). Kubo-takken
|
||||||
|
* voor die paden zouden dode code zijn die suggereert dat pins/uploads naar
|
||||||
|
* Kubo gaan — voeg ze dus niet terug zonder eerst `proxyIPFS()` te wijzigen
|
||||||
|
* én `isMutatingRequest()` in `write-auth.ts` opnieuw te controleren.
|
||||||
|
*
|
||||||
|
* `method` blijft onderdeel van de signature (schrijvers in `route.ts`,
|
||||||
|
* `write-auth.ts` en de tests geven hem door) maar wordt niet meer voor de
|
||||||
|
* vertaling gebruikt: geen enkele Kubo-mapping is nog methode-afhankelijk.
|
||||||
|
*/
|
||||||
export function mapToKuboAPI(path: string, method: string): string {
|
export function mapToKuboAPI(path: string, method: string): string {
|
||||||
// Normalize: /api/node/info → /api/v0/id, etc.
|
// Normalize: /api/node/info → /api/v0/id, etc.
|
||||||
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
@@ -213,16 +236,9 @@ export function mapToKuboAPI(path: string, method: string): string {
|
|||||||
return '/api/v0/id';
|
return '/api/v0/id';
|
||||||
case 'peers':
|
case 'peers':
|
||||||
return '/api/v0/swarm/peers';
|
return '/api/v0/swarm/peers';
|
||||||
case 'pins':
|
|
||||||
return method === 'GET' ? '/api/v0/pin/ls' : '/api/v0/pin/add';
|
|
||||||
default:
|
default:
|
||||||
if (p.startsWith('pins/')) {
|
// Alleen niet-`pins`/niet-`files/upload`-paden komen hier terecht; `files/*`
|
||||||
const cid = p.slice(5);
|
// voor niet-GET/DELETE-mutaties valt nog door naar Kubo.
|
||||||
return `/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`;
|
|
||||||
}
|
|
||||||
if (p === 'files/upload') {
|
|
||||||
return '/api/v0/add?pin=true';
|
|
||||||
}
|
|
||||||
if (p.startsWith('files/')) {
|
if (p.startsWith('files/')) {
|
||||||
return '/api/v0/ls';
|
return '/api/v0/ls';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user