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:
68
frontend/src/lib/auth.test.ts
Normal file
68
frontend/src/lib/auth.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user