From e5889df03eb2d475a780ee13c0a8d0490e04bd6b Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 14:36:45 -0400 Subject: [PATCH] test(10-02): add failing /api/me isAdmin+needsProviderSetup tests (RED) - dev-bypass path: isAdmin from DB (not hardcoded), needsProviderSetup from member_credentials - needsProviderSetup=true when no member_credentials row exists - needsProviderSetup=false when member_credentials row exists --- apps/api/tests/routes/me.test.ts | 108 ++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/apps/api/tests/routes/me.test.ts b/apps/api/tests/routes/me.test.ts index 51c94f8..646a7a1 100644 --- a/apps/api/tests/routes/me.test.ts +++ b/apps/api/tests/routes/me.test.ts @@ -1,5 +1,5 @@ /** - * GET /api/me — regression tests for dev-auth bypass path. + * GET /api/me — regression tests for dev-auth bypass path + isAdmin/needsProviderSetup * * Covers: * 1. DEV_AUTH_BYPASS=true (non-production): GET /api/me returns 200 with the injected @@ -8,6 +8,10 @@ * 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). + * 3. Plan 10-02 additions: + * - dev-bypass path returns isAdmin (DB-backed, not hardcoded) + needsProviderSetup + * - OIDC path returns isAdmin + needsProviderSetup + * - needsProviderSetup=true when no member_credentials row exists; false when one exists * * Architecture note: * devAuthBypass() and devBypassActive in index.ts both evaluate env vars at module @@ -20,13 +24,19 @@ 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. +// +// Default: select chain returns empty arrays (no rows). +// Per-test overrides: use vi.mocked(db.select).mockImplementation(...) to +// supply per-call sequences for isAdmin and memberCredentials lookups. // --------------------------------------------------------------------------- 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([]), + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]), @@ -138,3 +148,97 @@ describe('GET /api/me — OIDC path (no DEV_AUTH_BYPASS)', () => { expect(body.error).toBe('Unauthorized'); }); }); + +// --------------------------------------------------------------------------- +// Plan 10-02: isAdmin + needsProviderSetup on /api/me (D-03) +// --------------------------------------------------------------------------- + +describe('GET /api/me — isAdmin + needsProviderSetup (Plan 10-02, D-03)', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test'; + process.env.DEV_AUTH_BYPASS = 'true'; + }); + + it('dev-bypass: response includes isAdmin (DB-backed from users.is_admin, not hardcoded)', async () => { + // Set up db.select to return isAdmin=true for the users lookup, + // and [] for the memberCredentials lookup (needsProviderSetup=true). + const { db } = await import('../../src/db/client.js'); + + let callCount = 0; + vi.mocked(db.select).mockImplementation(() => { + callCount++; + const limitFn = callCount === 1 + ? vi.fn().mockResolvedValue([{ isAdmin: true }]) // users.isAdmin lookup + : vi.fn().mockResolvedValue([]); // memberCredentials lookup (none) + return { + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: limitFn }), + innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + + 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; isAdmin: boolean; needsProviderSetup: boolean } }; + expect(body.user).toHaveProperty('isAdmin'); + expect(body.user.isAdmin).toBe(true); // DB returns true, not hardcoded + }); + + it('dev-bypass: needsProviderSetup=true when no member_credentials row exists', async () => { + const { db } = await import('../../src/db/client.js'); + + let callCount = 0; + vi.mocked(db.select).mockImplementation(() => { + callCount++; + const limitFn = callCount === 1 + ? vi.fn().mockResolvedValue([{ isAdmin: false }]) // users.isAdmin lookup + : vi.fn().mockResolvedValue([]); // no member_credentials row + return { + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: limitFn }), + innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + + 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: { needsProviderSetup: boolean } }; + expect(body.user).toHaveProperty('needsProviderSetup'); + expect(body.user.needsProviderSetup).toBe(true); + }); + + it('dev-bypass: needsProviderSetup=false when a member_credentials row exists', async () => { + const { db } = await import('../../src/db/client.js'); + + let callCount = 0; + vi.mocked(db.select).mockImplementation(() => { + callCount++; + const limitFn = callCount === 1 + ? vi.fn().mockResolvedValue([{ isAdmin: false }]) // users.isAdmin lookup + : vi.fn().mockResolvedValue([{ id: 7 }]); // has member_credentials row + return { + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ limit: limitFn }), + innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + + 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: { needsProviderSetup: boolean } }; + expect(body.user).toHaveProperty('needsProviderSetup'); + expect(body.user.needsProviderSetup).toBe(false); + }); +});