File size: 2,655 Bytes
857fbc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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 { decodePathname } from '../../../src/lib/server/auth/decode-path';
import { requireAdmin, requireMemberTarget } from '../../../src/lib/server/auth/guards';
import { LocalFsStore } from '../../../src/lib/server/storage/local-fs-store';
import { UsersRepo, UserValidationError } from '../../../src/lib/server/users/repository';
import type { User } from '../../../src/lib/domain/types';

const localsFor = (user: User | null) => ({ user }) as unknown as App.Locals;

describe('requireAdmin', () => {
	const admin = { id: 'u_admin', role: 'admin' } as User;
	const member = { id: 'u_bob', role: 'member' } as User;

	it('returns the admin', () => {
		expect(requireAdmin(localsFor(admin))).toBe(admin);
	});

	it('rejects members and anonymous requests with 403', () => {
		for (const user of [member, null]) {
			expect(() => requireAdmin(localsFor(user))).toThrowError(
				expect.objectContaining({ status: 403 })
			);
		}
	});
});

describe('requireMemberTarget', () => {
	let dir: string;
	let repo: UsersRepo;
	let bobId: string;
	const clock = () => new Date('2026-08-29T12:00:00.000Z');

	beforeEach(async () => {
		dir = await mkdtemp(join(tmpdir(), 'splitwise-guards-'));
		repo = await UsersRepo.load(new LocalFsStore(dir), { clock });
		bobId = (await repo.create({ username: 'bob', displayName: 'Bob', password: 'temppass1' })).id;
	});
	afterEach(() => rm(dir, { recursive: true, force: true }));

	it('returns the member', () => {
		expect(requireMemberTarget(repo, bobId).username).toBe('bob');
	});

	it('rejects unknown ids and the admin account', () => {
		for (const id of ['', 'u_nope', 'u_admin']) {
			expect(() => requireMemberTarget(repo, id)).toThrowError(UserValidationError);
			try {
				requireMemberTarget(repo, id);
			} catch (e) {
				expect((e as UserValidationError).message).toBe('Unknown user');
				expect((e as UserValidationError).field).toBe('id');
			}
		}
	});
});

describe('decodePathname', () => {
	it('decodes escapes the way SvelteKit matches routes', () => {
		expect(decodePathname('/%61dmin')).toBe('/admin');
		expect(decodePathname('/%61dmin/storage')).toBe('/admin/storage');
		expect(decodePathname('/a%2Fb')).toBe('/a/b');
		expect(decodePathname('/admin')).toBe('/admin');
	});

	it('leaves an encoded percent sign intact', () => {
		expect(decodePathname('/x%25y')).toBe('/x%25y');
	});

	it('throws on a malformed escape', () => {
		expect(() => decodePathname('/%zz')).toThrowError(URIError);
	});
});