import { z } from 'zod'; import { test, expect } from '../fixtures'; import { BUDGET_MS, expectWithinBudget } from '../timing-budgets'; /** * FastAPI `/api/v1/auth/login` uses a *separate* user DB from NextAuth. * Set E2E_API_USERNAME + E2E_API_PASSWORD (or reuse admin FastAPI creds) to enable. * * Schemas mirror `backend/schemas/auth.py` (`TokenResponse` / `UserResponse`). */ const TokenResponse = z.object({ access_token: z.string().min(1), refresh_token: z.string().min(1), password_change_required: z.boolean(), }); const UserResponse = z.object({ username: z.string().min(1), is_admin: z.boolean(), role: z.string().min(1), permissions: z.record(z.boolean()), }); type TokenResponse = z.infer; type UserResponse = z.infer; test.describe('fastapi login @smoke', () => { test('login returns bearer token when API creds set', async ({ api, timings }) => { const username = process.env.E2E_API_USERNAME || ''; const password = process.env.E2E_API_PASSWORD || ''; test.skip(!username || !password, 'E2E_API_USERNAME/PASSWORD required for FastAPI auth'); const res = await timings.measure('api_login', () => api.post('/api/v1/auth/login', { body: { username, password }, expectedStatus: 200, schema: TokenResponse, }), ); expectWithinBudget(timings, 'api_login', BUDGET_MS.api); expect(res.data.access_token).toBeTruthy(); const authed = api.withAuthBearer(res.data.access_token); const me = await timings.measure('api_me', () => authed.get('/api/v1/auth/me', { expectedStatus: 200, schema: UserResponse }), ); expectWithinBudget(timings, 'api_me', BUDGET_MS.api); expect(me.status).toBe(200); expect(me.data.username).toBe(username); }); });