1b24bb2157
- Added functions for creating, updating, deleting, and listing collection entries in admin API. - Introduced seed data management for consistent test content across tests. - Updated global setup and teardown processes to ensure seeded content is created and cleaned up. - Refactored existing tests to utilize seeded content for improved reliability and maintainability.
166 lines
5.6 KiB
TypeScript
166 lines
5.6 KiB
TypeScript
import { APIRequestContext, request } from "@playwright/test"
|
|
import { ADMIN_TOKEN, API_BASE } from "../../fixtures/test-constants"
|
|
|
|
let adminContext: APIRequestContext | null = null
|
|
|
|
type CollectionEntry = {
|
|
id?: string
|
|
_id?: string | { $oid?: string }
|
|
[key: string]: unknown
|
|
}
|
|
|
|
type CollectionQueryOptions = {
|
|
filter?: Record<string, unknown>
|
|
sort?: string
|
|
limit?: number
|
|
offset?: number
|
|
projection?: string
|
|
lookup?: string
|
|
}
|
|
|
|
function getEntryId(entry: CollectionEntry): string | undefined {
|
|
if (typeof entry.id === "string") return entry.id
|
|
if (typeof entry._id === "string") return entry._id
|
|
if (entry._id && typeof entry._id === "object" && typeof entry._id.$oid === "string") return entry._id.$oid
|
|
return undefined
|
|
}
|
|
|
|
function buildCollectionUrl(collection: string, options: CollectionQueryOptions = {}, id?: string): string {
|
|
const searchParams = new URLSearchParams()
|
|
|
|
if (options.filter) searchParams.set("filter", JSON.stringify(options.filter))
|
|
if (options.sort) searchParams.set("sort", options.sort)
|
|
if (typeof options.limit === "number") searchParams.set("limit", String(options.limit))
|
|
if (typeof options.offset === "number") searchParams.set("offset", String(options.offset))
|
|
if (options.projection) searchParams.set("projection", options.projection)
|
|
if (options.lookup) searchParams.set("lookup", options.lookup)
|
|
|
|
const path = `${API_BASE}/${collection}${id ? `/${id}` : ""}`
|
|
const query = searchParams.toString()
|
|
return query ? `${path}?${query}` : path
|
|
}
|
|
|
|
async function parseJsonResponse<T>(
|
|
res: Awaited<ReturnType<APIRequestContext["get"]>>,
|
|
contextLabel: string
|
|
): Promise<T> {
|
|
const contentType = res.headers()["content-type"] || ""
|
|
if (contentType.includes("text/html")) {
|
|
throw new Error(`${contextLabel} returned HTML instead of JSON. Check CODING_URL or API proxy setup.`)
|
|
}
|
|
|
|
return (await res.json()) as T
|
|
}
|
|
|
|
/**
|
|
* Get or create a singleton admin API context with the ADMIN_TOKEN.
|
|
*/
|
|
export async function getAdminContext(baseURL: string): Promise<APIRequestContext> {
|
|
if (ADMIN_TOKEN === "CHANGE_ME") {
|
|
throw new Error("ADMIN_TOKEN is not configured for Playwright seed and cleanup")
|
|
}
|
|
|
|
if (!adminContext) {
|
|
adminContext = await request.newContext({
|
|
baseURL,
|
|
ignoreHTTPSErrors: true,
|
|
extraHTTPHeaders: {
|
|
Token: ADMIN_TOKEN,
|
|
},
|
|
})
|
|
}
|
|
return adminContext
|
|
}
|
|
|
|
export async function listCollectionEntries<T extends CollectionEntry = CollectionEntry>(
|
|
baseURL: string,
|
|
collection: string,
|
|
options: CollectionQueryOptions = {}
|
|
): Promise<T[]> {
|
|
const ctx = await getAdminContext(baseURL)
|
|
const res = await ctx.get(buildCollectionUrl(collection, options))
|
|
if (!res.ok()) {
|
|
throw new Error(`Failed to list ${collection}: ${res.status()} ${res.statusText()}`)
|
|
}
|
|
|
|
const body = await parseJsonResponse<unknown>(res, `Listing ${collection}`)
|
|
if (Array.isArray(body)) return body as T[]
|
|
if (body && typeof body === "object" && "data" in body && Array.isArray(body.data)) return body.data as T[]
|
|
return []
|
|
}
|
|
|
|
export async function createCollectionEntry<T extends CollectionEntry = CollectionEntry>(
|
|
baseURL: string,
|
|
collection: string,
|
|
entry: Record<string, unknown>
|
|
): Promise<T> {
|
|
const ctx = await getAdminContext(baseURL)
|
|
const res = await ctx.post(`${API_BASE}/${collection}`, { data: entry })
|
|
if (!res.ok()) {
|
|
throw new Error(`Failed to create ${collection}: ${res.status()} ${res.statusText()}`)
|
|
}
|
|
|
|
return await parseJsonResponse<T>(res, `Creating ${collection}`)
|
|
}
|
|
|
|
export async function updateCollectionEntry<T extends CollectionEntry = CollectionEntry>(
|
|
baseURL: string,
|
|
collection: string,
|
|
id: string,
|
|
entry: Record<string, unknown>
|
|
): Promise<T> {
|
|
const ctx = await getAdminContext(baseURL)
|
|
const res = await ctx.put(`${API_BASE}/${collection}/${id}`, { data: entry })
|
|
if (!res.ok()) {
|
|
throw new Error(`Failed to update ${collection}/${id}: ${res.status()} ${res.statusText()}`)
|
|
}
|
|
|
|
return await parseJsonResponse<T>(res, `Updating ${collection}/${id}`)
|
|
}
|
|
|
|
export async function deleteCollectionEntry(baseURL: string, collection: string, id: string): Promise<boolean> {
|
|
const ctx = await getAdminContext(baseURL)
|
|
const res = await ctx.delete(`${API_BASE}/${collection}/${id}`)
|
|
return res.ok()
|
|
}
|
|
|
|
export async function findFirstEntry<T extends CollectionEntry = CollectionEntry>(
|
|
baseURL: string,
|
|
collection: string,
|
|
options: CollectionQueryOptions = {}
|
|
): Promise<T | null> {
|
|
const entries = await listCollectionEntries<T>(baseURL, collection, { ...options, limit: 1 })
|
|
return entries[0] || null
|
|
}
|
|
|
|
export async function upsertCollectionEntry<T extends CollectionEntry = CollectionEntry>(
|
|
baseURL: string,
|
|
collection: string,
|
|
filter: Record<string, unknown>,
|
|
entry: Record<string, unknown>
|
|
): Promise<T> {
|
|
const existing = await findFirstEntry<T>(baseURL, collection, { filter })
|
|
const existingId = existing ? getEntryId(existing) : undefined
|
|
|
|
if (existingId) {
|
|
return updateCollectionEntry<T>(baseURL, collection, existingId, entry)
|
|
}
|
|
|
|
return createCollectionEntry<T>(baseURL, collection, entry)
|
|
}
|
|
|
|
export async function cleanupAllTestData(baseURL: string): Promise<{ users: number }> {
|
|
void baseURL
|
|
return { users: 0 }
|
|
}
|
|
|
|
/**
|
|
* Dispose the admin API context. Call in globalTeardown.
|
|
*/
|
|
export async function disposeAdminApi(): Promise<void> {
|
|
if (adminContext) {
|
|
await adminContext.dispose()
|
|
adminContext = null
|
|
}
|
|
}
|