Spaces:
Sleeping
Sleeping
assafvayner HF Staff
fix(app): 503 on storage failures, validated currency, lock release on shutdown, per-IP login limit, unit-only npm test
e830ad4 Download tests/unit/auth/auth.test.ts from assafvayner/splitwise: direct link, hf CLI and curl.
- Browser
- Download file 4.75 kB
-
https://huggingface.co/spaces/assafvayner/splitwise/resolve/main/tests/unit/auth/auth.test.ts
- Command line
-
hf download hf://spaces/assafvayner/splitwise/tests/unit/auth/auth.test.ts
-
curl -L -o auth.test.ts https://huggingface.co/spaces/assafvayner/splitwise/resolve/main/tests/unit/auth/auth.test.ts
4.75 kB
| import { describe, expect, it } from 'vitest'; | |
| import { hashPassword, verifyPassword } from '../../../src/lib/server/auth/password'; | |
| import { RateLimiter } from '../../../src/lib/server/auth/rate-limit'; | |
| import { createSessionToken, verifySessionToken } from '../../../src/lib/server/auth/session'; | |
| describe('password', () => { | |
| it('hashes and verifies', async () => { | |
| const hash = await hashPassword('correct horse'); | |
| expect(hash.startsWith('scrypt$')).toBe(true); | |
| expect(await verifyPassword('correct horse', hash)).toBe(true); | |
| expect(await verifyPassword('wrong', hash)).toBe(false); | |
| }); | |
| it('salts: same password gives different hashes', async () => { | |
| expect(await hashPassword('x')).not.toBe(await hashPassword('x')); | |
| }); | |
| it('rejects malformed hashes without throwing', async () => { | |
| expect(await verifyPassword('x', 'garbage')).toBe(false); | |
| }); | |
| it('rejects stored hashes with out-of-bound scrypt params', async () => { | |
| const placeholder = ['AAAAAAAAAAAAAAAAAAAAAA==', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=']; | |
| const bigP = ['scrypt', 32768, 8, 64, ...placeholder].join('$'); | |
| expect(await verifyPassword('whatever', bigP)).toBe(false); | |
| const bigN = ['scrypt', 131072, 8, 1, ...placeholder].join('$'); | |
| expect(await verifyPassword('whatever', bigN)).toBe(false); | |
| }); | |
| it('resolves 10 concurrent hashPassword calls (semaphore sanity)', async () => { | |
| const results = await Promise.all(Array.from({ length: 10 }, (_, i) => hashPassword(`pw-${i}`))); | |
| expect(results).toHaveLength(10); | |
| for (const hash of results) expect(hash.startsWith('scrypt$')).toBe(true); | |
| }); | |
| // 4 slots + a 64-deep wait queue: past that, callers are shed instead of parked forever. | |
| it('sheds callers once the scrypt wait queue is full', async () => { | |
| const settled = await Promise.allSettled(Array.from({ length: 70 }, (_, i) => hashPassword(`flood-${i}`))); | |
| const rejected = settled.filter((r) => r.status === 'rejected'); | |
| expect(rejected.length).toBeGreaterThan(0); | |
| for (const r of rejected) expect((r.reason as Error).message).toBe('Too many concurrent login attempts'); | |
| for (const r of settled.filter((r) => r.status === 'fulfilled')) { | |
| expect((r as PromiseFulfilledResult<string>).value.startsWith('scrypt$')).toBe(true); | |
| } | |
| }, 60_000); | |
| }); | |
| describe('session token', () => { | |
| const secret = 'test-secret-with-enough-length-0123456789'; | |
| it('round-trips a payload', () => { | |
| const token = createSessionToken({ uid: 'u_1', sv: 3 }, secret, new Date('2026-01-01T00:00:00Z'), 60); | |
| expect(verifySessionToken(token, secret, new Date('2026-01-01T00:00:30Z'))).toEqual({ uid: 'u_1', sv: 3, exp: Date.parse('2026-01-01T00:01:00Z') }); | |
| }); | |
| it('rejects tampering, wrong secret, and expiry', () => { | |
| const token = createSessionToken({ uid: 'u_1', sv: 3 }, secret, new Date('2026-01-01T00:00:00Z'), 60); | |
| expect(verifySessionToken(token + 'x', secret, new Date('2026-01-01T00:00:00Z'))).toBeNull(); | |
| expect(verifySessionToken(token, 'other-secret', new Date('2026-01-01T00:00:00Z'))).toBeNull(); | |
| expect(verifySessionToken(token, secret, new Date('2026-01-01T00:01:01Z'))).toBeNull(); | |
| const [payload, sig] = token.split('.'); | |
| const forged = Buffer.from(JSON.stringify({ ...JSON.parse(Buffer.from(payload, 'base64url').toString()), uid: 'u_admin' })).toString('base64url'); | |
| expect(verifySessionToken(`${forged}.${sig}`, secret, new Date('2026-01-01T00:00:00Z'))).toBeNull(); | |
| expect(verifySessionToken('nonsense', secret, new Date())).toBeNull(); | |
| }); | |
| }); | |
| describe('RateLimiter', () => { | |
| it('allows up to max attempts per window, then blocks until the window slides', () => { | |
| let now = 0; | |
| const rl = new RateLimiter(3, 60_000, () => now); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(false); | |
| expect(rl.allow('alice')).toBe(true); | |
| now = 60_001; | |
| expect(rl.allow('bob')).toBe(true); | |
| }); | |
| it('reset() clears a key so it is immediately allowed again', () => { | |
| let now = 0; | |
| const rl = new RateLimiter(5, 60_000, () => now); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(true); | |
| expect(rl.allow('bob')).toBe(false); | |
| rl.reset('bob'); | |
| expect(rl.allow('bob')).toBe(true); | |
| }); | |
| it('evicts stale keys once the tracked-key cap is exceeded', () => { | |
| let now = 0; | |
| const rl = new RateLimiter(5, 1_000, () => now); | |
| for (let i = 0; i < 10_001; i++) rl.allow(`key-${i}`); | |
| expect(rl.size).toBe(10_001); | |
| now = 2_000; // window has fully passed for every key inserted above | |
| expect(rl.allow('new-key')).toBe(true); | |
| expect(rl.size).toBe(1); | |
| }); | |
| }); | |