Phase 19: Local Auth (No-OIDC Mode) #23
@@ -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<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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user