/** * persistSessionCookie() middleware — unit tests. * * Tests the two critical behavioral properties: * A (persist path): when oidcAuthJwt is set on context, the response includes an * oidc-auth Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, Secure. * B (guard path): when oidcAuthJwt is NOT set, no oidc-auth Set-Cookie is emitted * (the no-resurrection security guard). * * Does NOT call @hono/oidc-auth — sets the context variable directly to keep this a * pure unit test that runs without MariaDB or any external service. */ import { describe, it, expect, afterEach } from 'vitest'; import { Hono } from 'hono'; import { persistSessionCookie } from '../../src/auth/persistSessionCookie.js'; // Read the cookie name the same way the implementation does so the test stays correct // if OIDC_COOKIE_NAME is overridden in the environment. const COOKIE_NAME = process.env.OIDC_COOKIE_NAME ?? 'oidc-auth'; afterEach(() => { // Nothing to restore: these tests do not mutate process.env. }); describe('persistSessionCookie middleware', () => { describe('Test A — persist path (oidcAuthJwt truthy)', () => { it('emits an oidc-auth Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, and Secure', async () => { const app = new Hono(); // Simulate what @hono/oidc-auth does: set a signed session JWT on context. app.use('/api/*', async (c, next) => { c.set('oidcAuthJwt' as never, 'header.payload.sig'); await next(); }); app.use('/api/*', persistSessionCookie()); app.get('/api/test', (c) => c.json({ ok: true })); const res = await app.request('/api/test'); expect(res.status).toBe(200); const setCookieHeader = res.headers.get('set-cookie'); expect(setCookieHeader).not.toBeNull(); // Cookie name must appear in the Set-Cookie value. expect(setCookieHeader).toContain(COOKIE_NAME); // Max-Age must be present (persistent cookie, not session-scoped). expect(setCookieHeader?.toLowerCase()).toContain('max-age'); // SameSite=Lax must be present. expect(setCookieHeader?.toLowerCase()).toContain('samesite=lax'); // HttpOnly must be present. expect(setCookieHeader?.toLowerCase()).toContain('httponly'); // Secure must be present. expect(setCookieHeader?.toLowerCase()).toContain('secure'); }); it('re-issues the same JWT value the library provided (no re-signing)', async () => { const dummyJwt = 'header.payload.sig'; const app = new Hono(); app.use('/api/*', async (c, next) => { c.set('oidcAuthJwt' as never, dummyJwt); await next(); }); app.use('/api/*', persistSessionCookie()); app.get('/api/test', (c) => c.json({ ok: true })); const res = await app.request('/api/test'); const setCookieHeader = res.headers.get('set-cookie') ?? ''; // The cookie value must contain the exact JWT string (URL-encoded = is fine but // the JWT characters must all appear). expect(setCookieHeader).toContain(dummyJwt); }); }); describe('Test B — guard path (oidcAuthJwt falsy / absent)', () => { it('emits NO oidc-auth Set-Cookie when oidcAuthJwt is not set (no resurrection)', async () => { const app = new Hono(); // Do NOT set oidcAuthJwt — simulates a logged-out or unauthenticated request. app.use('/api/*', persistSessionCookie()); app.get('/api/test', (c) => c.json({ ok: true })); const res = await app.request('/api/test'); expect(res.status).toBe(200); const setCookieHeader = res.headers.get('set-cookie'); // Either no Set-Cookie header at all, or it must not contain the oidc-auth cookie. const hasOidcCookie = setCookieHeader !== null && setCookieHeader.includes(COOKIE_NAME); expect(hasOidcCookie).toBe(false); }); it('emits NO Set-Cookie when oidcAuthJwt is explicitly set to empty string', async () => { const app = new Hono(); app.use('/api/*', async (c, next) => { c.set('oidcAuthJwt' as never, ''); await next(); }); app.use('/api/*', persistSessionCookie()); app.get('/api/test', (c) => c.json({ ok: true })); const res = await app.request('/api/test'); const setCookieHeader = res.headers.get('set-cookie'); const hasOidcCookie = setCookieHeader !== null && setCookieHeader.includes(COOKIE_NAME); expect(hasOidcCookie).toBe(false); }); }); });