From 0d8f3fa05160443e9ebae7f1bbecbfb975371d4c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 16:13:33 -0400 Subject: [PATCH] test(19-01): add failing tests for localSession JWT cookie helpers and assertLocalSessionSecretSet --- apps/api/tests/auth/localSession.test.ts | 168 +++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 apps/api/tests/auth/localSession.test.ts diff --git a/apps/api/tests/auth/localSession.test.ts b/apps/api/tests/auth/localSession.test.ts new file mode 100644 index 0000000..2d86a4d --- /dev/null +++ b/apps/api/tests/auth/localSession.test.ts @@ -0,0 +1,168 @@ +/** + * 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'); + }); +});