test(unit): broaden coverage — quota formula + auth token/JWT decode

Backend: extract the per-user quota formula from compute_storage_quota into a
pure `quota_limit_bytes(free_disk, tolerance, active)` (behavior-preserving) and
unit-test it: tolerance scaling, floor of fractional results, active<1 clamped to
1 (divide-by-zero guard), zero free disk, single-uploader identity.

Frontend (jsdom + browser:true mock): auth.ts token storage and JWT claim decode
— setAuth/getToken/getPin round-trip, clearAuth keeps the PIN (for recovery),
clearPin, getExpiry (exp seconds→ms Date; null on missing/malformed), getRole
(role claim; null on absent/malformed). Adds jsdom devDependency.

Backend: 25 passing. Frontend: 18 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-01 19:53:03 +02:00
parent 2c44006392
commit 226e455328
4 changed files with 644 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi } from 'vitest';
// auth.ts guards every localStorage access behind `browser`; force it true so the
// jsdom localStorage is actually used (the shared mock stubs it to false).
vi.mock('$app/environment', () => ({ browser: true, dev: false, building: false, version: 'test' }));
import { setAuth, getToken, getPin, getExpiry, getRole, clearAuth, clearPin } from './auth';
/** Build a JWT-shaped string (header.payload.sig) with the given claims. */
function makeJwt(claims: object): string {
const seg = (o: object) => btoa(JSON.stringify(o));
return `${seg({ alg: 'HS256', typ: 'JWT' })}.${seg(claims)}.sig`;
}
beforeEach(() => localStorage.clear());
describe('auth — token storage', () => {
it('setAuth then getToken/getPin round-trips', () => {
setAuth('a.b.c', '1234', 'uid-1', 'Alice');
expect(getToken()).toBe('a.b.c');
expect(getPin()).toBe('1234');
});
it('setAuth without a PIN leaves the PIN unset', () => {
setAuth('a.b.c', null, 'uid-1');
expect(getToken()).toBe('a.b.c');
expect(getPin()).toBeNull();
});
it('clearAuth removes the token but KEEPS the PIN (needed for recovery)', () => {
setAuth('a.b.c', '1234', 'uid');
clearAuth();
expect(getToken()).toBeNull();
expect(getPin()).toBe('1234');
});
it('clearPin removes the cached PIN', () => {
setAuth('a.b.c', '1234', 'uid');
clearPin();
expect(getPin()).toBeNull();
});
});
describe('auth — JWT claim decode', () => {
it('getExpiry decodes the exp claim (seconds → ms Date)', () => {
const exp = 2_000_000_000; // far-future unix seconds
setAuth(makeJwt({ exp }), null, 'u');
expect(getExpiry()?.getTime()).toBe(exp * 1000);
});
it('getExpiry is null with no token, no exp claim, or a malformed token', () => {
expect(getExpiry()).toBeNull(); // no token
setAuth(makeJwt({ role: 'guest' }), null, 'u'); // token without exp
expect(getExpiry()).toBeNull();
localStorage.setItem('eventsnap_jwt', 'not-a-jwt'); // unparseable
expect(getExpiry()).toBeNull();
});
it('getRole extracts the role claim; null when absent or malformed', () => {
setAuth(makeJwt({ role: 'host' }), null, 'u');
expect(getRole()).toBe('host');
setAuth(makeJwt({ exp: 1 }), null, 'u'); // no role claim
expect(getRole()).toBeNull();
localStorage.setItem('eventsnap_jwt', 'x.y.z'); // unparseable payload
expect(getRole()).toBeNull();
});
});