/** * Baserow pre-seed fixtures for e2e tests. * * Creates a minimal Baserow workspace + database + table + view + rows * that the e2e tests can target. All entities are created via the Baserow REST * API using the admin token obtained during the bootstrap sequence. * * Design choices: * - Idempotent: checks for existence before creating (lookup by name). * - Self-contained: all IDs are returned so tests do not have to hardcode them. * - Cleanup: the cleanup fixture deletes everything created here to avoid test pollution. */ import { request, type APIRequestContext } from "@playwright/test"; const BASEROW_URL = process.env.E2E_BASEROW_URL ?? "http://localhost:8081"; export interface BaserowSeed { workspaceId: number; databaseId: number; tableId: number; gridViewId: number; kanbanViewId: number; calendarViewId: number; /** Admin JWT for direct Baserow API calls. */ token: string; /** Pre-seeded row IDs (5 rows). */ rowIds: number[]; /** Name of the single-select field used for kanban grouping. */ singleSelectFieldName: string; /** Name of the date field used for calendar positioning. */ dateFieldName: string; /** Name of the primary text field. */ primaryFieldName: string; } export interface BaserowAdminCredentials { email: string; password: string; } export const baserowAdminCredentials: BaserowAdminCredentials = { email: process.env.E2E_BASEROW_ADMIN_EMAIL ?? "admin@acadenice-e2e.local", password: process.env.E2E_BASEROW_ADMIN_PASSWORD ?? "E2eAdminPassword123!", }; /** * Login to Baserow and return the JWT token. */ async function loginBaserow( apiContext: APIRequestContext, credentials: BaserowAdminCredentials, ): Promise { const response = await apiContext.post( `${BASEROW_URL}/api/user/token-auth/`, { data: credentials, headers: { "Content-Type": "application/json" }, }, ); if (!response.ok()) { const body = await response.text(); throw new Error(`Baserow login failed: HTTP ${response.status()} — ${body}`); } const data = (await response.json()) as { token?: string; access_token?: string }; const token = data.token ?? data.access_token; if (!token) { throw new Error(`Baserow login: no token in response ${JSON.stringify(data)}`); } return token; } /** * Register the first Baserow admin account (only works on a fresh instance). */ async function registerBaserowAdmin( apiContext: APIRequestContext, credentials: BaserowAdminCredentials, ): Promise { const response = await apiContext.post( `${BASEROW_URL}/api/user/`, { data: { email: credentials.email, password: credentials.password, name: "E2E Admin", authenticate: true, }, headers: { "Content-Type": "application/json" }, }, ); if (!response.ok()) { const body = await response.text(); throw new Error( `Baserow admin registration failed: HTTP ${response.status()} — ${body}`, ); } const data = (await response.json()) as { token?: string; access_token?: string }; return data.token ?? data.access_token ?? ""; } /** * Obtain a Baserow JWT — register on first boot, login on subsequent boots. */ export async function getBaserowToken( apiContext: APIRequestContext, ): Promise { // Try login first (most common case on subsequent runs). try { return await loginBaserow(apiContext, baserowAdminCredentials); } catch { // Login failed — probably a fresh Baserow instance with no users. try { const token = await registerBaserowAdmin(apiContext, baserowAdminCredentials); if (token) return token; // Registration succeeded but login required on a separate call. return await loginBaserow(apiContext, baserowAdminCredentials); } catch (registerError) { throw new Error( `Cannot obtain Baserow token: ${registerError instanceof Error ? registerError.message : String(registerError)}`, ); } } } /** * Ensure a Baserow workspace named "E2E Workspace" exists and return its ID. */ async function ensureWorkspace( apiContext: APIRequestContext, token: string, ): Promise { const headers = { Authorization: `JWT ${token}` }; const listResponse = await apiContext.get(`${BASEROW_URL}/api/workspaces/`, { headers }); if (!listResponse.ok()) { throw new Error(`List workspaces failed: ${listResponse.status()}`); } const workspaces = (await listResponse.json()) as Array<{ id: number; name: string }>; const existing = workspaces.find((w) => w.name === "E2E Workspace"); if (existing) return existing.id; const createResponse = await apiContext.post(`${BASEROW_URL}/api/workspaces/`, { headers: { ...headers, "Content-Type": "application/json" }, data: { name: "E2E Workspace" }, }); if (!createResponse.ok()) { throw new Error(`Create workspace failed: ${createResponse.status()}`); } const created = (await createResponse.json()) as { id: number }; return created.id; } /** * Ensure a database named "E2E Database" exists in the given workspace. */ async function ensureDatabase( apiContext: APIRequestContext, token: string, workspaceId: number, ): Promise { const headers = { Authorization: `JWT ${token}` }; const listResponse = await apiContext.get( `${BASEROW_URL}/api/applications/workspace/${workspaceId}/`, { headers }, ); if (listResponse.ok()) { const apps = (await listResponse.json()) as Array<{ id: number; name: string; type: string; }>; const existing = apps.find( (a) => a.name === "E2E Database" && a.type === "database", ); if (existing) return existing.id; } const createResponse = await apiContext.post(`${BASEROW_URL}/api/applications/workspace/${workspaceId}/`, { headers: { ...headers, "Content-Type": "application/json" }, data: { name: "E2E Database", type: "database" }, }); if (!createResponse.ok()) { throw new Error(`Create database failed: ${createResponse.status()}`); } const created = (await createResponse.json()) as { id: number }; return created.id; } /** * Ensure a table named "E2E Table" exists in the given database. * Returns the table ID and the IDs of the seeded fields. */ async function ensureTable( apiContext: APIRequestContext, token: string, databaseId: number, ): Promise<{ tableId: number; primaryFieldId: number; primaryFieldName: string; singleSelectFieldId: number; singleSelectFieldName: string; dateFieldId: number; dateFieldName: string; }> { const headers = { Authorization: `JWT ${token}` }; const listResponse = await apiContext.get( `${BASEROW_URL}/api/database/tables/database/${databaseId}/`, { headers }, ); if (listResponse.ok()) { const tables = (await listResponse.json()) as Array<{ id: number; name: string }>; const existing = tables.find((t) => t.name === "E2E Table"); if (existing) { // Fetch fields for the existing table. return await resolveTableFields(apiContext, token, existing.id); } } const createResponse = await apiContext.post( `${BASEROW_URL}/api/database/tables/database/${databaseId}/`, { headers: { ...headers, "Content-Type": "application/json" }, data: { name: "E2E Table", // Seed with column definitions upfront so we have a single_select + date field. data: [["Task", "Status", "Due Date"]], first_row_is_header: true, }, }, ); if (!createResponse.ok()) { const body = await createResponse.text(); throw new Error(`Create table failed: ${createResponse.status()} — ${body}`); } const created = (await createResponse.json()) as { id: number }; const tableId = created.id; // Add the single_select and date fields via field endpoints. await addSingleSelectField(apiContext, token, tableId); await addDateField(apiContext, token, tableId); return await resolveTableFields(apiContext, token, tableId); } async function addSingleSelectField( apiContext: APIRequestContext, token: string, tableId: number, ): Promise { const headers = { Authorization: `JWT ${token}`, "Content-Type": "application/json" }; await apiContext.post(`${BASEROW_URL}/api/database/fields/table/${tableId}/`, { headers, data: { name: "Status", type: "single_select", select_options: [ { value: "Todo", color: "blue" }, { value: "In Progress", color: "yellow" }, { value: "Done", color: "green" }, ], }, }); } async function addDateField( apiContext: APIRequestContext, token: string, tableId: number, ): Promise { const headers = { Authorization: `JWT ${token}`, "Content-Type": "application/json" }; await apiContext.post(`${BASEROW_URL}/api/database/fields/table/${tableId}/`, { headers, data: { name: "Due Date", type: "date", date_format: "ISO", }, }); } async function resolveTableFields( apiContext: APIRequestContext, token: string, tableId: number, ): Promise<{ tableId: number; primaryFieldId: number; primaryFieldName: string; singleSelectFieldId: number; singleSelectFieldName: string; dateFieldId: number; dateFieldName: string; }> { const headers = { Authorization: `JWT ${token}` }; const response = await apiContext.get( `${BASEROW_URL}/api/database/fields/table/${tableId}/`, { headers }, ); if (!response.ok()) { throw new Error(`List fields failed for table ${tableId}: ${response.status()}`); } const fields = (await response.json()) as Array<{ id: number; name: string; type: string; primary?: boolean; }>; const primaryField = fields.find((f) => f.primary) ?? fields[0]; const singleSelectField = fields.find((f) => f.type === "single_select"); const dateField = fields.find((f) => f.type === "date"); if (!primaryField || !singleSelectField || !dateField) { throw new Error( `Table ${tableId} missing required fields. Fields: ${JSON.stringify(fields.map((f) => ({ id: f.id, name: f.name, type: f.type })))}`, ); } return { tableId, primaryFieldId: primaryField.id, primaryFieldName: primaryField.name, singleSelectFieldId: singleSelectField.id, singleSelectFieldName: singleSelectField.name, dateFieldId: dateField.id, dateFieldName: dateField.name, }; } /** * Create grid, kanban, and calendar views for the table (or reuse existing). */ async function ensureViews( apiContext: APIRequestContext, token: string, tableId: number, ): Promise<{ gridViewId: number; kanbanViewId: number; calendarViewId: number }> { const headers = { Authorization: `JWT ${token}` }; const listResponse = await apiContext.get( `${BASEROW_URL}/api/database/views/table/${tableId}/`, { headers }, ); type ViewShape = { id: number; name: string; type: string }; let views: ViewShape[] = []; if (listResponse.ok()) { views = (await listResponse.json()) as ViewShape[]; } async function ensureView( name: string, type: string, extraData?: Record, ): Promise { const existing = views.find((v) => v.name === name && v.type === type); if (existing) return existing.id; const createResponse = await apiContext.post( `${BASEROW_URL}/api/database/views/table/${tableId}/`, { headers: { ...headers, "Content-Type": "application/json" }, data: { name, type, ...extraData }, }, ); if (!createResponse.ok()) { const body = await createResponse.text(); throw new Error(`Create ${type} view failed: ${createResponse.status()} — ${body}`); } const created = (await createResponse.json()) as { id: number }; return created.id; } const gridViewId = await ensureView("E2E Grid", "grid"); const kanbanViewId = await ensureView("E2E Kanban", "gallery"); // Baserow uses "gallery" for kanban-like const calendarViewId = await ensureView("E2E Calendar", "calendar"); return { gridViewId, kanbanViewId, calendarViewId }; } /** * Seed rows into the table. Returns the created row IDs. * Idempotent: if rows already exist (count >= 5), returns first 5 IDs. */ async function seedRows( apiContext: APIRequestContext, token: string, tableId: number, fields: { primaryFieldName: string; singleSelectFieldName: string; dateFieldName: string; }, ): Promise { const headers = { Authorization: `JWT ${token}` }; // Check existing rows first. const listResponse = await apiContext.get( `${BASEROW_URL}/api/database/rows/table/${tableId}/?user_field_names=true`, { headers }, ); if (listResponse.ok()) { const listData = (await listResponse.json()) as { count: number; results: Array<{ id: number }>; }; if (listData.count >= 5) { return listData.results.slice(0, 5).map((r) => r.id); } } const now = new Date(); const rowsToCreate = [ { [fields.primaryFieldName]: "Task Alpha", [fields.singleSelectFieldName]: "Todo", [fields.dateFieldName]: new Date(now.getTime() + 86_400_000) .toISOString() .slice(0, 10), }, { [fields.primaryFieldName]: "Task Beta", [fields.singleSelectFieldName]: "In Progress", [fields.dateFieldName]: new Date(now.getTime() + 2 * 86_400_000) .toISOString() .slice(0, 10), }, { [fields.primaryFieldName]: "Task Gamma", [fields.singleSelectFieldName]: "Done", [fields.dateFieldName]: new Date(now.getTime() + 3 * 86_400_000) .toISOString() .slice(0, 10), }, { [fields.primaryFieldName]: "Task Delta", [fields.singleSelectFieldName]: "Todo", [fields.dateFieldName]: new Date(now.getTime() + 4 * 86_400_000) .toISOString() .slice(0, 10), }, { [fields.primaryFieldName]: "Task Epsilon", [fields.singleSelectFieldName]: "In Progress", [fields.dateFieldName]: new Date(now.getTime() + 5 * 86_400_000) .toISOString() .slice(0, 10), }, ]; const rowIds: number[] = []; for (const rowData of rowsToCreate) { const createResponse = await apiContext.post( `${BASEROW_URL}/api/database/rows/table/${tableId}/?user_field_names=true`, { headers: { ...headers, "Content-Type": "application/json" }, data: rowData, }, ); if (!createResponse.ok()) { const body = await createResponse.text(); throw new Error(`Create row failed: ${createResponse.status()} — ${body}`); } const created = (await createResponse.json()) as { id: number }; rowIds.push(created.id); } return rowIds; } /** * Full pre-seed: workspace + database + table + views + rows. * Returns a BaserowSeed descriptor that tests use to target specific entities. */ export async function seedBaserow( apiContext: APIRequestContext, ): Promise { const token = await getBaserowToken(apiContext); const workspaceId = await ensureWorkspace(apiContext, token); const databaseId = await ensureDatabase(apiContext, token, workspaceId); const { tableId, primaryFieldName, singleSelectFieldName, dateFieldName, } = await ensureTable(apiContext, token, databaseId); const { gridViewId, kanbanViewId, calendarViewId } = await ensureViews( apiContext, token, tableId, ); const rowIds = await seedRows(apiContext, token, tableId, { primaryFieldName, singleSelectFieldName, dateFieldName, }); return { workspaceId, databaseId, tableId, gridViewId, kanbanViewId, calendarViewId, token, rowIds, singleSelectFieldName, dateFieldName, primaryFieldName, }; } /** * Update a single row directly via Baserow API (used in SSE tests). */ export async function updateRowViaBaserowApi( apiContext: APIRequestContext, token: string, tableId: number, rowId: number, data: Record, ): Promise { const response = await apiContext.patch( `${BASEROW_URL}/api/database/rows/table/${tableId}/${rowId}/?user_field_names=true`, { headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json", }, data, }, ); if (!response.ok()) { const body = await response.text(); throw new Error( `Baserow row update failed (table ${tableId}, row ${rowId}): ${response.status()} — ${body}`, ); } }