Files
familysync/apps/api/tests/auth/localSession.test.ts
T

169 lines
5.9 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;
// ── 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');
});
});