import type { APIRequestContext, APIResponse } from '@playwright/test'; import type { ApiTokenCreated, ApiTokenPublic, Credentials, SetupStatus, TokenScope, UserPublic, } from './data'; import { SESSION_SUCCESS } from './data'; async function requireStatus( response: APIResponse, expected: number ): Promise { if (response.status() !== expected) { throw new Error( `${response.url()} returned ${response.status()} ` + `${response.statusText()}, expected ${expected}` ); } return response; } export class E2EApi { constructor(readonly request: APIRequestContext) {} async setupStatus(): Promise { const response = await requireStatus( await this.request.get('/api/v1/setup/status'), 200 ); return response.json(); } async attemptLogin(credentials: Credentials): Promise { return this.request.post('/api/v1/auth/login', { form: { username: credentials.username, password: credentials.password, }, }); } async login(credentials: Credentials): Promise { const response = await requireStatus( await this.attemptLogin(credentials), 200 ); const payload = await response.json(); if (JSON.stringify(payload) !== JSON.stringify(SESSION_SUCCESS)) throw new Error('Login response did not match the session-only contract'); } async me(expectedStatus = 200): Promise { const response = await requireStatus( await this.request.get('/api/v1/auth/me'), expectedStatus ); return expectedStatus === 200 ? response.json() : null; } async logout(): Promise { await requireStatus(await this.request.post('/api/v1/auth/logout'), 200); } async updateCurrent(credentials: Credentials): Promise { const response = await requireStatus( await this.request.post('/api/v1/auth/update', { data: credentials, }), 200 ); const payload = await response.json(); if (JSON.stringify(payload) !== JSON.stringify(SESSION_SUCCESS)) throw new Error('Credential update exposed an unexpected response shape'); } async listUsers(): Promise { const response = await requireStatus( await this.request.get('/api/v1/users'), 200 ); return response.json(); } async createUser(credentials: Credentials): Promise { const response = await requireStatus( await this.request.post('/api/v1/users', { data: { ...credentials, enabled: true }, }), 201 ); return response.json(); } async deleteUser(userId: number): Promise { await requireStatus( await this.request.delete(`/api/v1/users/${userId}`), 204 ); } async createToken(name: string, scope: TokenScope): Promise { const response = await requireStatus( await this.request.post('/api/v1/tokens', { data: { name, scope }, }), 201 ); return response.json(); } async listTokens(): Promise { const response = await requireStatus( await this.request.get('/api/v1/tokens'), 200 ); return response.json(); } async revokeToken(tokenId: number): Promise { await requireStatus( await this.request.delete(`/api/v1/tokens/${tokenId}`), 204 ); } async bearerGet( path: string, token: string, expectedStatus: number ): Promise { return requireStatus( await this.request.get(path, { headers: { Authorization: `Bearer ${token}` }, }), expectedStatus ); } async disableMcpIpBypass(): Promise { const getResponse = await requireStatus( await this.request.get('/api/v1/config/get'), 200 ); const config = (await getResponse.json()) as Record & { security: Record; }; config.security.mcp_whitelist = []; await requireStatus( await this.request.patch('/api/v1/config/update', { data: config }), 200 ); } }