/** * localSession.ts + bootGuards.ts — unit tests for JWT session cookie helpers * and assertLocalSessionSecretSet boot guard. * * Uses Hono test app for cookie round-trips. All tests run without MariaDB. * * Test suite (TDD RED → GREEN — Plan 19-01 Task 2): * Test 1: issue then verify round-trips userId * Test 2: verifyLocalSessionCookie returns null when no local-session cookie present (no throw) * Test 3: verifyLocalSessionCookie returns null for a tampered/garbage token (no throw) * Test 4: assertLocalSessionSecretSet does NOT exit when DEV_AUTH_BYPASS='true' even if secret unset */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { Hono } from 'hono'; // ── Test constants ───────────────────────────────────────────────────────────── const TEST_SECRET = 'test-secret-that-is-at-least-32-characters-long-for-jwt'; const TEST_USER_ID = 42; // ── Helpers ──────────────────────────────────────────────────────────────────── /** Create a minimal Hono test app with an issue route and a verify route. */ function makeTestApp(secret: string | undefined) { return { setup: async () => { // Import inside function to pick up modified env const { issueLocalSessionCookie, verifyLocalSessionCookie } = await import( '../../src/auth/localSession.js' ); const app = new Hono(); app.post('/issue', async (c) => { await issueLocalSessionCookie(c, TEST_USER_ID); return c.json({ ok: true }); }); app.get('/verify', async (c) => { const userId = await verifyLocalSessionCookie(c); return c.json({ userId }); }); return app; }, }; } describe('issueLocalSessionCookie / verifyLocalSessionCookie', () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { originalEnv = { ...process.env }; process.env.LOCAL_SESSION_SECRET = TEST_SECRET; vi.resetModules(); // ensure fresh imports pick up env changes }); afterEach(() => { process.env = originalEnv; vi.resetModules(); }); it('Test 1: issue then verify round-trips userId', async () => { const { issueLocalSessionCookie, verifyLocalSessionCookie } = await import( '../../src/auth/localSession.js' ); const app = new Hono(); app.post('/issue', async (c) => { await issueLocalSessionCookie(c, TEST_USER_ID); return c.json({ ok: true }); }); app.get('/verify', async (c) => { const userId = await verifyLocalSessionCookie(c); return c.json({ userId }); }); // Issue cookie const issueRes = await app.request('/issue', { method: 'POST' }); expect(issueRes.status).toBe(200); const setCookieHeader = issueRes.headers.get('set-cookie'); expect(setCookieHeader).not.toBeNull(); expect(setCookieHeader).toContain('local-session='); // Extract cookie value and forward it for verify const cookieHeader = setCookieHeader?.split(';')[0]; // just name=value const verifyRes = await app.request('/verify', { headers: { cookie: cookieHeader ?? '' }, }); expect(verifyRes.status).toBe(200); const body = (await verifyRes.json()) as { userId: number | null }; expect(body.userId).toBe(TEST_USER_ID); }); it('Test 2: verifyLocalSessionCookie returns null when no local-session cookie present (no throw)', async () => { const { verifyLocalSessionCookie } = await import('../../src/auth/localSession.js'); const app = new Hono(); app.get('/verify', async (c) => { const userId = await verifyLocalSessionCookie(c); return c.json({ userId }); }); const res = await app.request('/verify'); expect(res.status).toBe(200); const body = (await res.json()) as { userId: number | null }; expect(body.userId).toBeNull(); }); it('Test 3: verifyLocalSessionCookie returns null for a tampered/garbage token (no throw)', async () => { const { verifyLocalSessionCookie } = await import('../../src/auth/localSession.js'); const app = new Hono(); app.get('/verify', async (c) => { const userId = await verifyLocalSessionCookie(c); return c.json({ userId }); }); // Send a garbage token — should return null without throwing const res = await app.request('/verify', { headers: { cookie: 'local-session=garbage.token.value' }, }); expect(res.status).toBe(200); const body = (await res.json()) as { userId: number | null }; expect(body.userId).toBeNull(); }); }); describe('assertLocalSessionSecretSet (bootGuards)', () => { let originalEnv: NodeJS.ProcessEnv; let exitSpy: ReturnType; beforeEach(() => { originalEnv = { ...process.env }; exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { throw new Error('process.exit called'); }) as never); vi.resetModules(); }); afterEach(() => { process.env = originalEnv; exitSpy.mockRestore(); vi.resetModules(); }); it('Test 4: assertLocalSessionSecretSet does NOT exit when DEV_AUTH_BYPASS=true even if secret unset', async () => { process.env.DEV_AUTH_BYPASS = 'true'; delete process.env.LOCAL_SESSION_SECRET; const { assertLocalSessionSecretSet } = await import('../../src/lib/bootGuards.js'); // Must not throw / must not call process.exit expect(() => assertLocalSessionSecretSet()).not.toThrow(); expect(exitSpy).not.toHaveBeenCalled(); }); it('assertLocalSessionSecretSet is exported from bootGuards', async () => { process.env.LOCAL_SESSION_SECRET = TEST_SECRET; delete process.env.DEV_AUTH_BYPASS; const bootGuards = await import('../../src/lib/bootGuards.js'); // Must export the function expect(typeof bootGuards.assertLocalSessionSecretSet).toBe('function'); }); });