aboutsummaryrefslogtreecommitdiff
path: root/test/storage.test.ts
blob: 985ba486f718cd1230477775b8a60110cc071563 (plain) (blame)
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);
    });
});