splitwise / tests /unit /storage /prefixed-store.test.ts
assafvayner's picture
assafvayner HF Staff
fix(storage): skip integration suite safely, tighten list contract, drop expand
d594446
Raw History Blame Contribute Delete
2.9 kB
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();
});
});