From df5d36308a753b0fd44ed853cf79fcee2b803bc6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 5 Jun 2026 13:48:05 -0400 Subject: [PATCH] fix(02): add regression tests for /api/me under dev-auth bypass - Asserts GET /api/me returns 200 with DEV_USER (id=1, color=#4A90D9) when DEV_AUTH_BYPASS=true and NODE_ENV!=production - Asserts oidcAuthMiddleware is NOT wired when bypass is active - Asserts oidcAuthMiddleware IS wired when bypass is absent - Asserts 401 from getAuth(null) fallback path with no OIDC session --- apps/api/tests/routes/me.test.ts | 143 +++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 apps/api/tests/routes/me.test.ts diff --git a/apps/api/tests/routes/me.test.ts b/apps/api/tests/routes/me.test.ts new file mode 100644 index 0000000..6700186 --- /dev/null +++ b/apps/api/tests/routes/me.test.ts @@ -0,0 +1,143 @@ +/** + * GET /api/me — regression tests for dev-auth bypass path. + * + * Covers: + * 1. DEV_AUTH_BYPASS=true (non-production): GET /api/me returns 200 with the injected + * DEV_USER identity (id=1, displayName='Dev User', color='#4A90D9'). + * The OIDC guard must NOT be enforced — no Authelia env vars needed. + * 2. Without DEV_AUTH_BYPASS: the OIDC middleware is still wired on /api/*. + * Verified structurally by asserting oidcAuthMiddleware is called during app init + * (the mock intercepts it and acts as a passthrough, confirming the mount path). + * + * Architecture note: + * devAuthBypass() and devBypassActive in index.ts both evaluate env vars at module + * load time. Tests must set process.env BEFORE importing the app module. + * vitest.resetModules() ensures each test gets a fresh module registry. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// --------------------------------------------------------------------------- +// Shared mock: DB — avoids real DB connections across all tests in this file. +// This mock is hoisted by Vitest and applies to every dynamic import below. +// --------------------------------------------------------------------------- +vi.mock('../../src/db/client.js', () => ({ + db: { + execute: vi.fn().mockResolvedValue([[{ '1': 1 }]]), + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + innerJoin: vi.fn().mockReturnValue({ + innerJoin: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue([]), + }), + }), + }), + }), + }, +})) + +// --------------------------------------------------------------------------- +// Track whether oidcAuthMiddleware was registered on the app. +// The spy is set up fresh per test via beforeEach/afterEach. +// --------------------------------------------------------------------------- +const oidcMiddlewareSpy = vi.fn( + () => async (_c: unknown, next: () => Promise) => next(), +) + +vi.mock('@hono/oidc-auth', () => ({ + oidcAuthMiddleware: () => oidcMiddlewareSpy(), + processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) => + c.json({ ok: true }), + getAuth: vi.fn().mockResolvedValue(null), +})) + +// --------------------------------------------------------------------------- +// Env snapshot — restored after each test to avoid cross-test pollution. +// --------------------------------------------------------------------------- +const originalNodeEnv = process.env.NODE_ENV +const originalBypassFlag = process.env.DEV_AUTH_BYPASS + +afterEach(() => { + process.env.NODE_ENV = originalNodeEnv + if (originalBypassFlag === undefined) { + delete process.env.DEV_AUTH_BYPASS + } else { + process.env.DEV_AUTH_BYPASS = originalBypassFlag + } + vi.resetModules() + oidcMiddlewareSpy.mockClear() +}) + +// --------------------------------------------------------------------------- + +describe('GET /api/me — dev-auth bypass (DEV_AUTH_BYPASS=true)', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test' + process.env.DEV_AUTH_BYPASS = 'true' + }) + + it('returns 200 with the injected dev user identity', async () => { + // Import AFTER setting env — index.ts reads env at module load time. + const { app } = await import('../../src/index.js') + const { DEV_USER } = await import('../../src/auth/devBypass.js') + + const res = await app.request('/api/me') + expect(res.status).toBe(200) + + const body = await res.json() as { user: { id: number; displayName: string; color: string } } + expect(body).toHaveProperty('user') + expect(body.user.id).toBe(DEV_USER.id) + expect(body.user.displayName).toBe(DEV_USER.displayName) + expect(body.user.color).toBe(DEV_USER.color) + }) + + it('returns id=1 and color=#4A90D9 (first palette slot)', async () => { + const { app } = await import('../../src/index.js') + + const res = await app.request('/api/me') + expect(res.status).toBe(200) + + const body = await res.json() as { user: { id: number; color: string } } + expect(body.user.id).toBe(1) + expect(body.user.color).toBe('#4A90D9') + }) + + it('does not invoke oidcAuthMiddleware on /api/* when bypass is active', async () => { + const { app } = await import('../../src/index.js') + + // Hit any /api/* route to trigger the middleware stack. + await app.request('/api/me') + + // oidcAuthMiddleware() factory must NOT have been called — index.ts skips it. + expect(oidcMiddlewareSpy).not.toHaveBeenCalled() + }) +}) + +describe('GET /api/me — OIDC path (no DEV_AUTH_BYPASS)', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test' + delete process.env.DEV_AUTH_BYPASS + }) + + it('wires oidcAuthMiddleware on /api/* when bypass is not active', async () => { + // Import app — devBypassActive will be false, so oidcAuthMiddleware() is called + // during app construction (index.ts registers it via app.use('/api/*', ...)). + await import('../../src/index.js') + + // The spy wraps the oidcAuthMiddleware() factory call in index.ts. + // It must have been called exactly once (one app.use registration). + expect(oidcMiddlewareSpy).toHaveBeenCalledTimes(1) + }) + + it('returns 401 when no OIDC session is present (getAuth returns null)', async () => { + const { app } = await import('../../src/index.js') + + // oidcAuthMiddleware is mocked as a passthrough; getAuth is mocked to return null. + // me.ts falls through to the getAuth path and returns 401. + const res = await app.request('/api/me') + expect(res.status).toBe(401) + const body = await res.json() as { error: string } + expect(body.error).toBe('Unauthorized') + }) +})