import { World, setWorldConstructor, type IWorldOptions } from '@cucumber/cucumber' import { strict as assert } from 'assert' import { apiGet, apiPost, apiPut, apiDelete } from './api-client.js' import { config } from './config.js' import { getUserId, TEST_PASSWORD } from './db.js' export class PerausetWorld extends World { lastResponse: Response | null = null lastBody: any = null cookies: string = '' currentUserId: string = '' currentEmail: string = '' createdUsers: Record = {} lastAssignmentId: string = '' constructor(options: IWorldOptions) { super(options) } /** * Authenticate as the given user via better-auth's email/password sign-in. * POST /api/auth/sign-in/email with JSON {email, password} and capture the * session cookie from the response. */ async login(email: string): Promise { const res = await fetch(`${config.apiUrl}/api/auth/sign-in/email`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', // better-auth requires a trusted Origin for state-changing auth calls. Origin: config.apiUrl, }, body: JSON.stringify({ email, password: TEST_PASSWORD }), redirect: 'manual', }) if (!res.ok) { const text = await res.text().catch(() => '') throw new Error(`Login failed for ${email}: ${res.status} ${text}`) } this.cookies = this.extractSetCookies(res) this.currentEmail = email } /** * Look up the current user's id from the SQLite DB using the stored email. */ async getSessionUserId(): Promise { assert.ok(this.currentEmail, 'No current email set -- did you call login() first?') return getUserId(this.currentEmail) } async get(path: string): Promise { this.lastResponse = await apiGet(config.apiUrl, path, this.cookies) this.lastBody = await this.lastResponse.clone().json().catch(() => null) this.captureCookies() } async post(path: string, body: any): Promise { this.lastResponse = await apiPost(config.apiUrl, path, body, this.cookies) this.lastBody = await this.lastResponse.clone().json().catch(() => null) this.captureCookies() } async put(path: string, body: any): Promise { this.lastResponse = await apiPut(config.apiUrl, path, body, this.cookies) this.lastBody = await this.lastResponse.clone().json().catch(() => null) this.captureCookies() } async delete(path: string): Promise { this.lastResponse = await apiDelete(config.apiUrl, path, this.cookies) this.lastBody = await this.lastResponse.clone().json().catch(() => null) this.captureCookies() } expectStatus(status: number): void { assert.ok(this.lastResponse, 'No response received') assert.equal( this.lastResponse.status, status, `Expected status ${status} but got ${this.lastResponse.status}. Body: ${JSON.stringify(this.lastBody)}` ) } private captureCookies(): void { const setCookie = this.lastResponse?.headers.get('set-cookie') if (setCookie) { this.cookies = this.mergeCookies(this.cookies, setCookie) } } private extractSetCookies(res: Response): string { // getSetCookie() returns individual set-cookie values as an array const raw = (res.headers as any).getSetCookie?.() as string[] | undefined if (raw && raw.length > 0) { return raw.map((c: string) => c.split(';')[0]).join('; ') } // Fallback: use get('set-cookie') which may be comma-joined const header = res.headers.get('set-cookie') if (!header) return '' return header .split(/,(?=\s*\w+=)/) .map((c: string) => c.split(';')[0].trim()) .join('; ') } private mergeCookies(existing: string, incoming: string): string { if (!existing) return incoming if (!incoming) return existing const map = new Map() for (const part of existing.split(';').map((s) => s.trim()).filter(Boolean)) { const [name] = part.split('=', 1) map.set(name, part) } for (const part of incoming.split(';').map((s) => s.trim()).filter(Boolean)) { const [name] = part.split('=', 1) map.set(name, part) } return [...map.values()].join('; ') } } setWorldConstructor(PerausetWorld)