From cdb1a57614068fcfefa145bc6df45c9def7ccc6a Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 14 Dec 2025 22:43:24 -0800 Subject: Updates --- test/storage.test.ts | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/storage.test.ts (limited to 'test/storage.test.ts') diff --git a/test/storage.test.ts b/test/storage.test.ts new file mode 100644 index 0000000..7b64aa1 --- /dev/null +++ b/test/storage.test.ts @@ -0,0 +1,106 @@ +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); + }); +}); -- cgit v1.2.3-70-g09d2