feat: enhance admin API helpers with CRUD operations for collections and seed data management

- 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.
This commit is contained in:
2026-05-12 20:36:06 +00:00
parent 491f495c66
commit 1b24bb2157
17 changed files with 697 additions and 444 deletions
+127 -38
View File
@@ -3,10 +3,63 @@ 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.
*/
async function getAdminContext(baseURL: string): Promise<APIRequestContext> {
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,
@@ -19,50 +72,86 @@ async function getAdminContext(baseURL: string): Promise<APIRequestContext> {
return adminContext
}
/**
* Delete a user by ID via admin API.
*/
export async function deleteUser(baseURL: string, userId: string): Promise<boolean> {
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.delete(`${API_BASE}/user/${userId}`)
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()
}
/**
* Cleanup test users matching a pattern in their email.
*/
export async function cleanupTestUsers(baseURL: string, emailPattern: RegExp | string): Promise<number> {
const ctx = await getAdminContext(baseURL)
const res = await ctx.get(`${API_BASE}/user`)
if (!res.ok()) return 0
const users: { id?: string; _id?: string; email?: string }[] = await res.json()
if (!Array.isArray(users)) return 0
let deleted = 0
for (const user of users) {
const email = user.email || ""
const matches = typeof emailPattern === "string" ? email.includes(emailPattern) : emailPattern.test(email)
if (matches) {
const userId = user.id || user._id
if (userId) {
const ok = await deleteUser(baseURL, userId)
if (ok) deleted++
}
}
}
return deleted
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)
}
/**
* Cleanup all test data (users and tokens matching @test.example.com).
*/
export async function cleanupAllTestData(baseURL: string): Promise<{ users: number }> {
const testPattern = "@test.example.com"
const users = await cleanupTestUsers(baseURL, testPattern)
return { users }
void baseURL
return { users: 0 }
}
/**