Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
8.1 kB ยท 297 lines
TypeScript
at apifix
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298import { and, db, desc, eq, inArray } from "@openstatus/db";import { type Scope, auditLog, notification, page, pageComponent, selectWorkspaceSchema, workspace as workspaceTable,} from "@openstatus/db/src/schema";import { expect } from "@std/expect";
import type { Actor, DB, DrizzleTx, ServiceContext } from "../src/context";import type { Workspace } from "../src/types";
class RollbackSignal extends Error {}
/** * Wrap a test body in a transaction that always rolls back, so per-test * writes (rows, audit log entries, etc.) never reach the committed db. * * Usage: * await withTestTransaction(async (tx) => { * const ctx = { ...makeUserCtx(workspace), db: tx }; * await createPage({ ctx, input }); * await expectAuditRow({ ..., db: tx }); * }); * * Constraints: * - Top-level test wrapper only. Don't call inside service code or * nest two `withTestTransaction` calls โ libsql HTTP doesn't * support savepoints. * - Inside the callback, replace any `db.select(...)` / * `db.insert(...)` / `db.delete(...)` with the same call on `tx`. * Reads via committed `db` will not see the in-tx writes. */export async function withTestTransaction<T>( fn: (tx: DrizzleTx) => Promise<T>,): Promise<T> { let result!: T; try { await db.transaction(async (tx) => { result = await fn(tx); throw new RollbackSignal(); }); } catch (e) { if (!(e instanceof RollbackSignal)) throw e; } return result;}
/** * Clear leftover quota-gated rows on a workspace so tests that rely on * a specific cap state (e.g. `free` plan has `status-pages: 1`, * `notification-channels: 1`) can run regardless of what prior tests * or aborted runs left behind. * * Intended for `beforeAll` of suites that exercise the `free` * workspace โ the tight-plan negative tests break randomly otherwise * because cumulative state trips a quota check before the test can * hit its intended assertion. Scoped to the two tables that have bit * us repeatedly in CI; extend when a new quota-gated table surfaces. */export async function cleanQuotaGatedTables( workspaceId: number,): Promise<void> { // Pages โ delete dependent pageComponents first to satisfy FKs. const pages = await db .select({ id: page.id }) .from(page) .where(eq(page.workspaceId, workspaceId)) .all(); const pageIds = pages.map((p) => p.id); if (pageIds.length > 0) { await db .delete(pageComponent) .where(inArray(pageComponent.pageId, pageIds)) .catch(() => undefined); await db .delete(page) .where(inArray(page.id, pageIds)) .catch(() => undefined); } await db .delete(notification) .where(eq(notification.workspaceId, workspaceId)) .catch(() => undefined);}
/** Wipe audit_log rows for a workspace. Call in `beforeEach` / `afterEach`. */export async function clearAuditLog( workspaceId: number, opts: { db?: DB } = {},): Promise<void> { const conn = opts.db ?? db; await conn .delete(auditLog) .where(eq(auditLog.workspaceId, workspaceId)) .catch(() => undefined);}
/** * Delete audit_log rows for specific (entityType, entityId) pairs. * * Tests that create entities through services on the committed db and * then delete those entities in cleanup must also call this โ otherwise * the audit row outlives the entity, and because SQLite recycles * INTEGER PRIMARY KEY ids after deletes, a later test's freshly-inserted * entity can land on the orphan's id and inherit its actor attribution. * See docs/adr/test-audit-cleanup.md. */export async function clearAuditLogFor(args: { entityType: string; entityIds: ReadonlyArray<string | number>; db?: DB;}): Promise<void> { if (args.entityIds.length === 0) return; const conn = args.db ?? db; await conn .delete(auditLog) .where( and( eq(auditLog.entityType, args.entityType), inArray( auditLog.entityId, args.entityIds.map((id) => String(id)), ), ), ) .catch(() => undefined);}
/** * Load a seeded workspace by id (defaults to id=1, the `team` plan fixture). * Tests that need a fresh workspace should insert one explicitly and clean up; * isolating every test with its own workspace is the prevailing convention. */export async function loadSeededWorkspace(id = 1): Promise<Workspace> { const row = await db .select() .from(workspaceTable) .where(eq(workspaceTable.id, id)) .get(); if (!row) { throw new Error( `loadSeededWorkspace(${id}): workspace not found. Did you seed the db?`, ); } return selectWorkspaceSchema.parse(row);}
export function makeUserCtx( workspace: Workspace, opts: { userId: number; requestId?: string } = { userId: 1 },): ServiceContext { return { workspace, actor: { type: "user", userId: opts.userId }, requestId: opts.requestId, };}
export function makeApiKeyCtx( workspace: Workspace, opts: { keyId: string; userId?: number; requestId?: string; scopes?: Scope[]; },): ServiceContext { return { workspace, actor: { type: "apiKey", keyId: opts.keyId, userId: opts.userId, scopes: opts.scopes ?? ["write"], }, requestId: opts.requestId, };}
export function makeMcpCtx( workspace: Workspace, opts: { keyId: string; userId?: number; requestId?: string; scopes?: Scope[]; },): ServiceContext { return { workspace, actor: { type: "mcp", keyId: opts.keyId, userId: opts.userId, scopes: opts.scopes ?? ["write"], }, requestId: opts.requestId, };}
export function makeSlackCtx( workspace: Workspace, opts: { teamId: string; slackUserId: string; userId?: number; requestId?: string; },): ServiceContext { return { workspace, actor: { type: "slack", teamId: opts.teamId, slackUserId: opts.slackUserId, userId: opts.userId, }, requestId: opts.requestId, };}
export function makeSystemCtx( workspace: Workspace, opts: { job: string; requestId?: string },): ServiceContext { return { workspace, actor: { type: "system", job: opts.job }, requestId: opts.requestId, };}
/** * Read audit rows for a workspace, optionally filtered by entity. * Sort is `(id DESC)` โ the autoincrement id is monotonic within the * test DB and gives deterministic order even for rows written in the * same millisecond. */export async function readAuditLog(filter: { workspaceId: number; entityType?: string; entityId?: string | number; db?: DB;}): Promise<(typeof auditLog.$inferSelect)[]> { const conn = filter.db ?? db; const clauses = [eq(auditLog.workspaceId, filter.workspaceId)]; if (filter.entityType !== undefined) { clauses.push(eq(auditLog.entityType, filter.entityType)); } if (filter.entityId !== undefined) { clauses.push(eq(auditLog.entityId, String(filter.entityId))); } return conn .select() .from(auditLog) .where(and(...clauses)) .orderBy(desc(auditLog.id)) .all();}
/** * Assert an audit row matching `match` was recorded during the test. * Queries the real `audit_log` table โ ensure tests clean rows per * workspace (see `clearAuditLog`) or rely on fresh entity ids to * avoid cross-test matches. */export async function expectAuditRow(match: { workspaceId: number; action: string; entityType: string; entityId: string | number; actorType?: Actor["type"]; db?: DB;}): Promise<void> { const expectedEntityId = String(match.entityId); const rows = await readAuditLog({ workspaceId: match.workspaceId, entityType: match.entityType, entityId: expectedEntityId, db: match.db, }); const hit = rows.find( (row) => row.action === match.action && (match.actorType === undefined || row.actorType === match.actorType), ); if (hit === undefined) { throw new Error( `expected audit row for ${match.action} on ${match.entityType}#${expectedEntityId}`, ); } expect(hit).toBeDefined();}