1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
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);
});
});
|