1import { describe, expect, it } from 'vitest';2import {3 clearSessionCookieHeader,4 hashToken,5 mintTokenValue,6 parseCookies,7 SESSION_COOKIE,8 sessionCookieHeader,9 signSession,10 verifySession,11} from '../src/auth';1213const SECRET = 'test-secret';14const future = () => Math.floor(Date.now() / 1000) + 3600;1516describe('session cookie signing', () => {17 it('round-trips a valid session', async () => {18 const value = await signSession(SECRET, 'usr_abc123', future());19 expect(await verifySession(SECRET, value)).toBe('usr_abc123');20 });2122 it('rejects a tampered payload', async () => {23 const value = await signSession(SECRET, 'usr_abc123', future());24 const [, sig] = value.split('.');25 const forgedPayload = Buffer.from(JSON.stringify({ u: 'usr_evil', exp: future() }))26 .toString('base64url');27 expect(await verifySession(SECRET, `${forgedPayload}.${sig}`)).toBeNull();28 });2930 it('rejects a signature from another secret', async () => {31 const value = await signSession('other-secret', 'usr_abc123', future());32 expect(await verifySession(SECRET, value)).toBeNull();33 });3435 it('rejects an expired session', async () => {36 const value = await signSession(SECRET, 'usr_abc123', Math.floor(Date.now() / 1000) - 1);37 expect(await verifySession(SECRET, value)).toBeNull();38 });3940 it('rejects garbage without throwing', async () => {41 expect(await verifySession(SECRET, '')).toBeNull();42 expect(await verifySession(SECRET, 'no-dot')).toBeNull();43 expect(await verifySession(SECRET, 'not!base64.???')).toBeNull();44 expect(await verifySession(SECRET, 'YWJj.YWJj')).toBeNull();45 });46});4748describe('cookie headers', () => {49 it('sets HttpOnly SameSite=Lax with Secure only when asked', () => {50 const h = sessionCookieHeader('v', { secure: true, maxAge: 60 });51 expect(h).toContain(`${SESSION_COOKIE}=v`);52 expect(h).toContain('HttpOnly');53 expect(h).toContain('SameSite=Lax');54 expect(h).toContain('Secure');55 expect(sessionCookieHeader('v', { secure: false, maxAge: 60 })).not.toContain('Secure');56 });5758 it('clears with Max-Age=0', () => {59 expect(clearSessionCookieHeader(false)).toContain('Max-Age=0');60 });6162 it('parses cookie headers', () => {63 const map = parseCookies(`a=1; ${SESSION_COOKIE}=abc.def; b=2`);64 expect(map.get(SESSION_COOKIE)).toBe('abc.def');65 expect(parseCookies(null).size).toBe(0);66 });67});6869describe('api tokens', () => {70 it('mints trb_ + exactly 32 base64url chars', () => {71 for (let i = 0; i < 20; i++) {72 expect(mintTokenValue()).toMatch(/^trb_[A-Za-z0-9_-]{32}$/);73 }74 });7576 it('mints unique values', () => {77 const seen = new Set(Array.from({ length: 100 }, () => mintTokenValue()));78 expect(seen.size).toBe(100);79 });8081 it('hashes tokens with sha-256 (known vector)', async () => {82 // sha256("abc")83 expect(await hashToken('abc')).toBe(84 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',85 );86 });87});