import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, readFile, rm } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { Storage } from '../src/storage/index.js'; import { ContentType, type RouteConfig } from '../src/types/index.js'; describe('Storage', () => { let dataDir: string; beforeEach(async () => { dataDir = await mkdtemp(join(tmpdir(), 'posthook-test-')); }); afterEach(async () => { await rm(dataDir, { recursive: true, force: true }); }); it('persists routes to routes.json and loads them on init', async () => { const route: RouteConfig = { name: 'route1', contentType: ContentType.JSON, hcaptchaProtected: false, }; const storage1 = new Storage(dataDir); expect((await storage1.init()).left().present()).toBe(false); expect((await storage1.registerRoute(route)).left().present()).toBe(false); const storage2 = new Storage(dataDir); expect((await storage2.init()).left().present()).toBe(false); expect(storage2.getRoute('route1')).toEqual(route); expect(storage2.listRoutes()).toEqual([route]); }); it('rejects unsafe route names', async () => { const storage = new Storage(dataDir); expect((await storage.init()).left().present()).toBe(false); const result = await storage.registerRoute({ name: '../bad', contentType: ContentType.JSON, hcaptchaProtected: false, }); expect(result.left().present()).toBe(true); expect(result.left().get().message).toBe('Invalid route name'); }); it('stores a request and sanitizes uploaded filenames', async () => { const route: RouteConfig = { name: 'route1', contentType: ContentType.JSON, hcaptchaProtected: false, }; const storage = new Storage(dataDir); expect((await storage.init()).left().present()).toBe(false); expect((await storage.registerRoute(route)).left().present()).toBe(false); const upload = { fieldName: 'file', filename: '../evil.txt', contentType: 'text/plain', size: 3, data: new Uint8Array([1, 2, 3]), }; const storeResult = await storage.storeRequest( 'route1', 'POST', { 'content-type': 'application/json' }, { hello: 'world' }, [upload], ); expect(storeResult.left().present()).toBe(false); const stored = storeResult.right().get(); expect(stored.routeName).toBe('route1'); expect(stored.files?.length).toBe(1); const storedFile = stored.files![0]; expect(storedFile.filename).toMatch(/^0_/); expect(storedFile.filename).not.toContain('..'); expect(storedFile.filename).not.toContain('/'); expect(storedFile.path).toBe(`files/${storedFile.filename}`); const requestDir = join(dataDir, 'route1', `${stored.timestamp}_${stored.uuid}`); const requestJson = JSON.parse(await readFile(join(requestDir, 'request.json'), 'utf-8')) as { routeName: string; files?: Array<{ filename: string }>; }; expect(requestJson.routeName).toBe('route1'); expect(requestJson.files?.[0].filename).toBe(storedFile.filename); const bodyJson = JSON.parse(await readFile(join(requestDir, 'body.json'), 'utf-8')); expect(bodyJson).toEqual({ hello: 'world' }); const savedBytes = await readFile(join(requestDir, storedFile.path)); expect(savedBytes.length).toBe(3); }); });