File size: 2,895 Bytes
0d57d79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d594446
 
 
 
0d57d79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
874bd3c
 
 
 
 
0d57d79
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
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { LocalFsStore } from '../../../src/lib/server/storage/local-fs-store';
import { PrefixedStore } from '../../../src/lib/server/storage/prefixed-store';
import { decodeText, encodeText } from '../../../src/lib/server/storage/store';

describe('PrefixedStore', () => {
	let dir: string;
	let inner: LocalFsStore;
	let a: PrefixedStore;
	let b: PrefixedStore;
	beforeEach(async () => {
		dir = await mkdtemp(join(tmpdir(), 'splitwise-prefixed-'));
		inner = new LocalFsStore(dir);
		a = new PrefixedStore(inner, 'tenants/a');
		b = new PrefixedStore(inner, '/tenants/b/');
	});
	afterEach(() => rm(dir, { recursive: true, force: true }));

	it('stores objects under the prefix and reads them back', async () => {
		await a.put('users.json', encodeText('A'), 'application/json');
		expect(decodeText((await a.get('users.json'))!)).toBe('A');
		expect(decodeText((await inner.get('tenants/a/users.json'))!)).toBe('A');
		expect(await b.get('users.json')).toBeNull();
	});

	it('lists with the prefix stripped and never leaks other prefixes', async () => {
		await a.put('events/1.json', encodeText('1'));
		await a.put('events/2.json', encodeText('2'));
		await b.put('events/9.json', encodeText('9'));
		expect((await a.list('events/')).map((e) => e.path)).toEqual(['events/1.json', 'events/2.json']);
		expect((await a.list('')).map((e) => e.path)).toEqual(['events/1.json', 'events/2.json']);
		expect((await b.list('events/')).map((e) => e.path)).toEqual(['events/9.json']);
	});

	it('rejects a list prefix without a trailing slash', async () => {
		await expect(a.list('events')).rejects.toThrow(/Invalid prefix/);
	});

	it('appends, copies and deletes inside the prefix', async () => {
		await a.append('ledger.jsonl', encodeText('x\n'));
		await a.append('ledger.jsonl', encodeText('y\n'));
		await a.copy('ledger.jsonl', 'backups/l.jsonl');
		expect(decodeText((await inner.get('tenants/a/backups/l.jsonl'))!)).toBe('x\ny\n');
		await a.delete(['ledger.jsonl', 'missing.txt']);
		expect(await a.get('ledger.jsonl')).toBeNull();
	});

	it('wipe removes only its own subtree', async () => {
		await a.put('x.txt', encodeText('x'));
		await a.put('deep/y.txt', encodeText('y'));
		await b.put('z.txt', encodeText('z'));
		expect(await a.wipe()).toBe(2);
		expect(await a.list('')).toEqual([]);
		expect(await b.get('z.txt')).not.toBeNull();
	});

	it('rejects invalid prefixes', () => {
		expect(() => new PrefixedStore(inner, '')).toThrow();
		expect(() => new PrefixedStore(inner, '../x')).toThrow();
	});

	it('rejects paths that escape the prefix', async () => {
		await expect(a.get('../b/z.txt')).rejects.toThrow();
		await expect(a.put('../../x', encodeText('x'))).rejects.toThrow();
	});
});