/** * QA-C5: Auth storage module tests (replaces the Redux authSlice tests). * * Tests the sessionStorage-backed session persistence (WEB-C1): * - setAuthUser/getAuthUser round-trip * - getAuthUser tolerates malformed stored JSON * - clearAuth removes the stored session * - isTokenValid accepts unexpired JWTs and rejects expired/malformed ones */ import { describe, it, expect, beforeEach } from 'vitest'; import type { AuthUser } from '@proposal-system/api-contracts'; import { getAuthUser, setAuthUser, clearAuth, isTokenValid } from '../authStorage'; import { STORAGE_KEY_TOKEN } from '../../../constants'; function createToken(expOffsetSeconds: number): string { const header = btoa(JSON.stringify({ alg: 'HS256' })); const payload = btoa(JSON.stringify({ sub: 'test-user', exp: Math.floor(Date.now() / 1000) + expOffsetSeconds, })); return `${header}.${payload}.fake-signature`; } const user: AuthUser = { id: 'user-1', email: 'admin@test.com', displayName: 'Admin', role: 'Admin', token: createToken(3600), }; describe('authStorage', () => { beforeEach(() => { window.sessionStorage.clear(); }); it('QA-C5: setAuthUser persists to sessionStorage and getAuthUser reads it back', () => { setAuthUser(user); expect(window.sessionStorage.getItem(STORAGE_KEY_TOKEN)).not.toBeNull(); expect(getAuthUser()).toEqual(user); }); it('QA-C5: getAuthUser returns null when nothing is stored', () => { expect(getAuthUser()).toBeNull(); }); it('QA-C5: getAuthUser returns null for malformed stored JSON', () => { window.sessionStorage.setItem(STORAGE_KEY_TOKEN, 'not-valid-json{{{'); expect(getAuthUser()).toBeNull(); }); it('QA-C5: clearAuth removes the stored session', () => { setAuthUser(user); clearAuth(); expect(window.sessionStorage.getItem(STORAGE_KEY_TOKEN)).toBeNull(); expect(getAuthUser()).toBeNull(); }); it('QA-C5: isTokenValid accepts a token expiring in the future', () => { expect(isTokenValid(createToken(3600))).toBe(true); }); it('QA-C5: isTokenValid rejects an expired token', () => { expect(isTokenValid(createToken(-3600))).toBe(false); }); it('isTokenValid accepts a base64url payload (JWT segments are base64url, not base64)', () => { // '>>>' encodes to 'Pj4-' in base64url — the '-' made the old atob() call throw. const payload = btoa(JSON.stringify({ sub: '>>>>>>', exp: Math.floor(Date.now() / 1000) + 3600 })) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); expect(payload).toMatch(/[-_]/); expect(isTokenValid(`${btoa(JSON.stringify({ alg: 'HS256' }))}.${payload}.sig`)).toBe(true); }); it('QA-C5: isTokenValid rejects missing or malformed tokens', () => { expect(isTokenValid(null)).toBe(false); expect(isTokenValid(undefined)).toBe(false); expect(isTokenValid('')).toBe(false); expect(isTokenValid('not-a-jwt')).toBe(false); }); });