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); }); });