splitwise / tests /unit /auth /guards.test.ts
assafvayner's picture
assafvayner HF Staff
fix(auth): gate admin routes on the decoded path and in the actions themselves
857fbc2
Raw History Blame Contribute Delete
2.66 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 { 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);
});
});