Files
Lucas BergerandClaude Opus 4.8 b6490feff4
CI / changes (pull_request) Successful in 9s
CI / api (pull_request) Successful in 3m2s
CI / fast-checks (pull_request) Successful in 4m20s
CI / security (pull_request) Successful in 1m14s
CI / harness (pull_request) Successful in 6m56s
CI / gate (pull_request) Successful in 2s
fix(19): satisfy CI fast-checks + secret scan
Lint (eslint --max-warnings 0):
- index.ts: disable no-unsafe-argument on the type-only Context mismatch when
  delegating to the OIDC handler inside the local-session skip wrapper
- localAuth.ts: handleLogout is sync (no await) — drop async (require-await)
- devBypass.ts: disable detect-possible-timing-attacks on the public well-known
  dev-placeholder string compare (not a secret comparison)
- remove dead code / unused bindings flagged by no-unused-vars: makeTestApp
  (localSession.test), makeUnauthContext + BrowserContext import (login.spec),
  unused memberId (admin.test), unused txSelectCount counter (me.test)
- localAuthMiddleware.test / me.test: fix unused + reflow-detached
  eslint-disable directives

Format: prettier --write across the 20 Phase-19 files that were never formatted.

Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char
TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key
regex hit on "credential atomically, 409-equivalent"). Neither is a real secret.

Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks),
PWA 266/266, API 452/452.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:05:15 -04:00

141 lines
5.0 KiB
TypeScript

/**
* 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;
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<typeof vi.spyOn>;
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');
});
});