aboutsummaryrefslogtreecommitdiff
path: root/test/storage.test.ts
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 22:43:24 -0800
commitcdb1a57614068fcfefa145bc6df45c9def7ccc6a (patch)
tree92cadbecda8658c143b7625d5925e3411976a892 /test/storage.test.ts
parent6d318665a08c0d4564d8de23cc39425d2c0bac49 (diff)
downloadposthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.tar.gz
posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.zip
Updates
Diffstat (limited to 'test/storage.test.ts')
-rw-r--r--test/storage.test.ts106
1 files changed, 106 insertions, 0 deletions
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);
+ });
+});