import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, readFile, rm, writeFile } 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; let configPath: string; beforeEach(async () => { dataDir = await mkdtemp(join(tmpdir(), 'posthook-test-')); configPath = join(dataDir, 'routes.toml'); }); afterEach(async () => { await rm(dataDir, { recursive: true, force: true }); }); it('loads routes from routes.toml on init', async () => { const route: RouteConfig = { name: 'route1', contentType: ContentType.JSON, hcaptchaProtected: false, }; // Create a TOML config file const toml = ` [[route]] name = "route1" contentType = "json" hcaptchaProtected = false `; await writeFile(configPath, toml); const storage = new Storage(dataDir, configPath); expect((await storage.init()).left().present()).toBe(false); expect(storage.getRoute('route1')).toEqual(route); expect(storage.listRoutes()).toEqual([route]); }); it('returns undefined for unsafe route names', async () => { const toml = ` [[route]] name = "safe-route" contentType = "json" hcaptchaProtected = false `; await writeFile(configPath, toml); const storage = new Storage(dataDir, configPath); expect((await storage.init()).left().present()).toBe(false); // getRoute should reject unsafe names expect(storage.getRoute('../bad')).toBeUndefined(); expect(storage.getRoute('.')).toBeUndefined(); expect(storage.getRoute('..')).toBeUndefined(); expect(storage.getRoute('path/with/slash')).toBeUndefined(); }); it('stores a request and sanitizes uploaded filenames', async () => { const toml = ` [[route]] name = "route1" contentType = "json" hcaptchaProtected = false `; await writeFile(configPath, toml); const storage = new Storage(dataDir, configPath); expect((await storage.init()).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 savedBytes = await readFile(join(requestDir, storedFile.path)); expect(savedBytes.length).toBe(3); }); });