125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
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<string, string> = {}
|
|
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<void> {
|
|
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<string> {
|
|
assert.ok(this.currentEmail, 'No current email set -- did you call login() first?')
|
|
return getUserId(this.currentEmail)
|
|
}
|
|
|
|
async get(path: string): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string, string>()
|
|
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)
|