import type { Page, Route } from "@playwright/test"; export type ClientStatus = "active" | "inactive"; export type ClientType = "private" | "pkce"; export type Client = { id: string; name: string; type: ClientType; status: ClientStatus; redirectUris: string[]; scopes: string[]; createdAt: string; clientSecret?: string; metadata?: Record; }; export type Consent = { subject: string; userName: string; clientId: string; clientName: string; grantedScopes: string[]; authenticatedAt?: string; createdAt: string; deletedAt?: string; status: "active" | "revoked"; tenantId: string; tenantName: string; }; export type AuditLog = { event_id: string; timestamp: string; user_id: string; event_type: string; status: "success" | "failure"; ip_address: string; user_agent: string; details: string; }; export type DevApiMockState = { clients: Client[]; consents: Consent[]; auditLogsByCursor?: Record< string, { items: AuditLog[]; next_cursor?: string } >; auditLogs?: AuditLog[]; onUpdateStatus?: (status: ClientStatus) => void; onRotateSecret?: (newSecret: string) => void; }; export function makeClient( id: string, overrides: Partial = {}, ): Client { return { id, name: `${id} app`, type: "private", status: "active", redirectUris: [`https://${id}.example.com/callback`], scopes: ["openid", "profile", "email"], createdAt: "2026-03-03T00:00:00.000Z", clientSecret: `${id}-secret`, metadata: {}, ...overrides, }; } export async function seedAuth(page: Page, role?: string) { const nowInSeconds = Math.floor(Date.now() / 1000); await page.addInitScript( ({ issuedAt, injectedRole }) => { const mockOidcUser = { id_token: "playwright-id-token", session_state: "playwright-session", access_token: "playwright-access-token", refresh_token: "playwright-refresh-token", token_type: "Bearer", scope: "openid profile email", profile: { sub: "playwright-user", email: "playwright@example.com", name: "Playwright User", ...(injectedRole ? { role: injectedRole } : {}), }, expires_at: issuedAt + 3600, }; window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc:devfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem( "oidc.user:http://localhost:5000/oidc/:devfront", JSON.stringify(mockOidcUser), ); window.localStorage.setItem("dev_tenant_id", "tenant-a"); }, { issuedAt: nowInSeconds, injectedRole: role ?? "" }, ); await page.route("**/oidc/**", async (route) => { const url = route.request().url(); if (url.includes(".well-known/openid-configuration")) { await route.fulfill({ json: { issuer: "http://localhost:5000/oidc", authorization_endpoint: "http://localhost:5000/oidc/auth", token_endpoint: "http://localhost:5000/oidc/token", jwks_uri: "http://localhost:5000/oidc/jwks", userinfo_endpoint: "http://localhost:5000/oidc/userinfo", end_session_endpoint: "http://localhost:5000/oidc/session/end", }, headers: { "Access-Control-Allow-Origin": "*" }, }); } else if (url.includes("/jwks")) { await route.fulfill({ json: { keys: [] }, headers: { "Access-Control-Allow-Origin": "*" }, }); } else { await route.fulfill({ status: 200, body: "ok", headers: { "Access-Control-Allow-Origin": "*" }, }); } }); } function json(route: Route, payload: unknown, status = 200) { return route.fulfill({ status, contentType: "application/json", body: JSON.stringify(payload), }); } function parseClientId(pathname: string): string { const parts = pathname.split("/").filter(Boolean); return parts[parts.length - 1] ?? ""; } export async function installDevApiMock(page: Page, state: DevApiMockState) { const appendAuditLog = ( eventType: string, action: string, targetId: string, status: "success" | "failure" = "success", ) => { if (!state.auditLogs) return; state.auditLogs.unshift({ event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, timestamp: new Date().toISOString(), user_id: "playwright-user", event_type: eventType, status, ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action, target_id: targetId, tenant_id: "tenant-a", }), }); }; await page.route("**/api/v1/dev/**", async (route) => { const request = route.request(); const url = new URL(request.url()); const { pathname, searchParams } = url; const method = request.method(); if (pathname === "/api/v1/dev/stats" && method === "GET") { const total = state.clients.length; return json(route, { total_clients: total, active_sessions: Math.max(1, total), auth_failures_24h: 0, }); } if (pathname === "/api/v1/dev/clients" && method === "GET") { return json(route, { items: state.clients.map((client) => ({ id: client.id, name: client.name, type: client.type, status: client.status, createdAt: client.createdAt, redirectUris: client.redirectUris, scopes: client.scopes, })), limit: 50, offset: 0, }); } if (pathname === "/api/v1/dev/clients" && method === "POST") { const payload = (request.postDataJSON() as { name?: string; type?: ClientType; status?: ClientStatus; redirectUris?: string[]; scopes?: string[]; metadata?: Record; }) || { name: "created app" }; const created = makeClient(`client-${state.clients.length + 1}`, { name: payload.name ?? "created app", type: payload.type ?? "private", status: payload.status ?? "active", redirectUris: payload.redirectUris ?? [], scopes: payload.scopes ?? ["openid"], metadata: payload.metadata ?? {}, }); state.clients.push(created); appendAuditLog("CLIENT_CREATE", "CREATE_CLIENT", created.id); return json(route, { client: created, endpoints: { discovery: "https://issuer/.well-known/openid-configuration", issuer: "https://issuer", authorization: "https://issuer/oauth2/auth", token: "https://issuer/oauth2/token", userinfo: "https://issuer/userinfo", }, }); } if ( pathname.startsWith("/api/v1/dev/clients/") && pathname.endsWith("/status") && method === "PATCH" ) { const clientId = pathname.split("/")[5] ?? ""; const payload = request.postDataJSON() as { status: ClientStatus }; const found = state.clients.find((client) => client.id === clientId); if (!found) return json(route, { error: "not found" }, 404); found.status = payload.status; appendAuditLog("CLIENT_UPDATE_STATUS", "UPDATE_CLIENT_STATUS", clientId); state.onUpdateStatus?.(payload.status); return json(route, { client: found, endpoints: { discovery: "https://issuer/.well-known/openid-configuration", issuer: "https://issuer", authorization: "https://issuer/oauth2/auth", token: "https://issuer/oauth2/token", userinfo: "https://issuer/userinfo", }, }); } if ( pathname.startsWith("/api/v1/dev/clients/") && pathname.endsWith("/secret/rotate") && method === "POST" ) { const clientId = pathname.split("/")[5] ?? ""; const found = state.clients.find((client) => client.id === clientId); if (!found) return json(route, { error: "not found" }, 404); found.clientSecret = `${clientId}-rotated-secret`; appendAuditLog("CLIENT_ROTATE_SECRET", "ROTATE_SECRET", clientId); state.onRotateSecret?.(found.clientSecret); return json(route, { client: found, endpoints: { discovery: "https://issuer/.well-known/openid-configuration", issuer: "https://issuer", authorization: "https://issuer/oauth2/auth", token: "https://issuer/oauth2/token", userinfo: "https://issuer/userinfo", }, }); } if (pathname.startsWith("/api/v1/dev/clients/") && method === "PUT") { const clientId = parseClientId(pathname); const payload = (request.postDataJSON() as { name?: string; type?: ClientType; scopes?: string[]; redirectUris?: string[]; metadata?: Record; }) || { name: "updated app" }; const found = state.clients.find((client) => client.id === clientId); if (!found) return json(route, { error: "not found" }, 404); if (payload.name) found.name = payload.name; if (payload.type) found.type = payload.type; if (payload.scopes) found.scopes = payload.scopes; if (payload.redirectUris) found.redirectUris = payload.redirectUris; if (payload.metadata) found.metadata = payload.metadata; appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId); return json(route, { client: found, endpoints: { discovery: "https://issuer/.well-known/openid-configuration", issuer: "https://issuer", authorization: "https://issuer/oauth2/auth", token: "https://issuer/oauth2/token", userinfo: "https://issuer/userinfo", }, }); } if (pathname.startsWith("/api/v1/dev/clients/") && method === "DELETE") { const clientId = parseClientId(pathname); state.clients = state.clients.filter((client) => client.id !== clientId); appendAuditLog("CLIENT_DELETE", "DELETE_CLIENT", clientId); return route.fulfill({ status: 204 }); } if (pathname.startsWith("/api/v1/dev/clients/") && method === "GET") { const clientId = parseClientId(pathname); const found = state.clients.find((client) => client.id === clientId); if (!found) return json(route, { error: "forbidden" }, 403); return json(route, { client: found, endpoints: { discovery: "https://issuer/.well-known/openid-configuration", issuer: "https://issuer", authorization: "https://issuer/oauth2/auth", token: "https://issuer/oauth2/token", userinfo: "https://issuer/userinfo", }, }); } if (pathname === "/api/v1/dev/consents" && method === "GET") { const subject = searchParams.get("subject") || ""; const clientId = searchParams.get("client_id") || ""; const status = searchParams.get("status") || ""; const items = state.consents.filter((row) => { const matchesSubject = !subject || row.subject.includes(subject) || row.userName.includes(subject); const matchesClientId = !clientId || row.clientId === clientId; const matchesStatus = !status || row.status === status; return matchesSubject && matchesClientId && matchesStatus; }); return json(route, { items }); } if (pathname === "/api/v1/dev/consents" && method === "DELETE") { const subject = searchParams.get("subject") || ""; const clientId = searchParams.get("client_id") || ""; const target = state.consents.find( (row) => row.subject === subject && row.clientId === clientId, ); if (target) { target.status = "revoked"; target.deletedAt = "2026-03-03T10:00:00.000Z"; } return route.fulfill({ status: 204 }); } if (pathname === "/api/v1/dev/audit-logs" && method === "GET") { if (state.auditLogsByCursor) { const cursor = searchParams.get("cursor") || ""; const pageSet = state.auditLogsByCursor[cursor] ?? { items: [] }; return json(route, { items: pageSet.items, limit: 50, cursor: cursor || undefined, next_cursor: pageSet.next_cursor, }); } if (state.auditLogs) { const action = searchParams.get("action") || ""; const clientId = searchParams.get("client_id") || ""; const status = searchParams.get("status") || ""; const filtered = state.auditLogs.filter((item) => { let parsedDetails: { action?: string; target_id?: string } = {}; try { parsedDetails = JSON.parse(item.details) as { action?: string; target_id?: string; }; } catch {} const matchesAction = !action || parsedDetails.action === action; const matchesClient = !clientId || parsedDetails.target_id === clientId; const matchesStatus = !status || item.status === status; return matchesAction && matchesClient && matchesStatus; }); return json(route, { items: filtered, limit: 50 }); } return json(route, { items: [], limit: 50 }); } return json(route, { error: `Unhandled ${method} ${pathname}` }, 404); }); }