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.
20 kB ยท 616 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617import { db, eq, inArray } from "@openstatus/db";import { maintenance, maintenancesToPageComponents, page, pageComponent, pageComponentGroup, statusReport, statusReportUpdate, statusReportsToPageComponents,} from "@openstatus/db/src/schema";import { allPlans } from "@openstatus/db/src/schema/plan/config";import type { Limits } from "@openstatus/db/src/schema/plan/schema";import { createPage, createTestWorkspace,} from "@openstatus/db/src/test/factories";import { MOCK_COMPONENTS, MOCK_COMPONENT_GROUPS, MOCK_INCIDENTS, MOCK_PAGES, MOCK_SUBSCRIBERS,} from "@openstatus/importers/statuspage/fixtures";import { clearAuditLogFor } from "@openstatus/services/test/helpers";import { expect } from "@std/expect";import { afterAll, beforeAll, test } from "@std/testing/bdd";
import { edgeRouter } from "../edge";import { createInnerTRPCContext } from "../trpc";
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
const originalFetch = globalThis.fetch;
function mockStatuspageFetch() { globalThis.fetch = ((url: string | URL | Request) => { const urlStr = typeof url === "string" ? url : url.toString(); let data: unknown = [];
// For paginated endpoints, return empty on page > 1 const pageParam = urlStr.includes("?") ? new URL(urlStr).searchParams.get("page") : null; const isFirstPage = !pageParam || pageParam === "1";
if (urlStr.endsWith("/pages")) data = MOCK_PAGES; else if (urlStr.includes("/component-groups")) data = isFirstPage ? MOCK_COMPONENT_GROUPS : []; else if (urlStr.includes("/components")) data = isFirstPage ? MOCK_COMPONENTS : []; else if (urlStr.includes("/incidents/scheduled")) data = isFirstPage ? MOCK_INCIDENTS.filter((i) => i.scheduled_for != null) : []; else if (urlStr.includes("/incidents")) data = isFirstPage ? MOCK_INCIDENTS : []; else if (urlStr.includes("/subscribers")) data = isFirstPage ? MOCK_SUBSCRIBERS : [];
return Promise.resolve( new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" }, }), ); }) as typeof globalThis.fetch;}
function restoreFetch() { globalThis.fetch = originalFetch;}
// Own workspace + page: the import assertions count page components, which// only holds if no sibling suite writes to the same page.let TEST_WORKSPACE_ID: number;let OTHER_WORKSPACE_ID: number;let TEST_PAGE_ID: number;
function makeCaller( limitsOverride?: Partial<Limits>, workspaceId = TEST_WORKSPACE_ID,) { const limits = { ...allPlans.starter.limits, ...limitsOverride }; const ctx = createInnerTRPCContext({ req: undefined, session: { user: { id: "1" } }, // @ts-expect-error - minimal workspace for test workspace: { id: workspaceId, limits }, // Populate `user` too so the `NODE_ENV=test` escape hatch in // `enforceUserIsAuthed` takes โ otherwise the middleware would // fall through to the DB read and replace our override limits // with the seeded team-plan workspace, making every assertion // below a no-op against the real team plan. // @ts-expect-error - minimal user for test user: { id: 1 }, }); return edgeRouter.createCaller(ctx);}
// Track all created IDs for cleanupconst createdIds = { pages: [] as number[], componentGroups: [] as number[], components: [] as number[], statusReports: [] as number[], maintenances: [] as number[],};
async function cleanup() { // Delete in reverse dependency order
// 1. statusReportsToPageComponents if (createdIds.statusReports.length > 0) { await db .delete(statusReportsToPageComponents) .where( inArray( statusReportsToPageComponents.statusReportId, createdIds.statusReports, ), ); }
// 2. maintenancesToPageComponents if (createdIds.maintenances.length > 0) { await db .delete(maintenancesToPageComponents) .where( inArray( maintenancesToPageComponents.maintenanceId, createdIds.maintenances, ), ); }
// 3. statusReportUpdate (via statusReportId). Capture ids first so we // can wipe their audit rows โ `statusReportUpdate.id` recycles after // delete, and an orphan audit row would re-attribute a later // unrelated update. let statusReportUpdateIds: number[] = []; if (createdIds.statusReports.length > 0) { statusReportUpdateIds = ( await db .select({ id: statusReportUpdate.id }) .from(statusReportUpdate) .where( inArray(statusReportUpdate.statusReportId, createdIds.statusReports), ) .all() ).map((r) => r.id); await db .delete(statusReportUpdate) .where( inArray(statusReportUpdate.statusReportId, createdIds.statusReports), ); }
// 4. statusReport if (createdIds.statusReports.length > 0) { await db .delete(statusReport) .where(inArray(statusReport.id, createdIds.statusReports)); }
// 5. maintenance if (createdIds.maintenances.length > 0) { await db .delete(maintenance) .where(inArray(maintenance.id, createdIds.maintenances)); }
// 6. pageComponent if (createdIds.components.length > 0) { await db .delete(pageComponent) .where(inArray(pageComponent.id, createdIds.components)); }
// 7. pageComponentGroup if (createdIds.componentGroups.length > 0) { await db .delete(pageComponentGroup) .where(inArray(pageComponentGroup.id, createdIds.componentGroups)); }
// 8. page (only ones we created, never the seeded page 1) if (createdIds.pages.length > 0) { await db.delete(page).where(inArray(page.id, createdIds.pages)); }
// 9. audit_log: the importer service emits one audit row per created // entity. The entities are gone but the rows are not โ and // INTEGER PRIMARY KEY ids recycle, so a later test inserting into the // same table can land on an id that already has an audit row, // inheriting its actor attribution. See packages/services/AGENTS.md. await Promise.all([ clearAuditLogFor({ entityType: "page", entityIds: createdIds.pages }), clearAuditLogFor({ entityType: "page_component_group", entityIds: createdIds.componentGroups, }), clearAuditLogFor({ entityType: "page_component", entityIds: createdIds.components, }), clearAuditLogFor({ entityType: "status_report", entityIds: createdIds.statusReports, }), clearAuditLogFor({ entityType: "status_report_update", entityIds: statusReportUpdateIds, }), clearAuditLogFor({ entityType: "maintenance", entityIds: createdIds.maintenances, }), ]);
// Reset trackers createdIds.pages = []; createdIds.componentGroups = []; createdIds.components = []; createdIds.statusReports = []; createdIds.maintenances = [];}
/** Collect IDs from import summary phases for cleanup tracking. */function trackCreatedIds(summary: { phases: Array<{ phase: string; resources: Array<{ openstatusId?: number; status: string }>; }>;}) { for (const phase of summary.phases) { const ids = phase.resources .filter((r) => r.status === "created" && r.openstatusId) .map((r) => r.openstatusId as number);
switch (phase.phase) { case "page": createdIds.pages.push(...ids); break; case "componentGroups": createdIds.componentGroups.push(...ids); break; case "components": createdIds.components.push(...ids); break; case "incidents": createdIds.statusReports.push(...ids); break; case "maintenances": createdIds.maintenances.push(...ids); break; } }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
let caller: ReturnType<typeof makeCaller>;
beforeAll(async () => { mockStatuspageFetch(); TEST_WORKSPACE_ID = (await createTestWorkspace()).workspace.id; OTHER_WORKSPACE_ID = (await createTestWorkspace()).workspace.id; TEST_PAGE_ID = (await createPage(TEST_WORKSPACE_ID)).id; caller = makeCaller();});
afterAll(async () => { restoreFetch(); await cleanup();});
test("preview returns mapped data without DB writes", async () => { const result = await caller.import.preview({ provider: "statuspage", apiKey: "test-key", });
expect(result.provider).toBe("statuspage"); expect(result.phases.length).toBeGreaterThan(0);
// Check that expected phases exist const phaseNames = result.phases.map((p) => p.phase); expect(phaseNames).toContain("page"); expect(phaseNames).toContain("components"); expect(phaseNames).toContain("componentGroups");
// Verify resource counts match fixture data const pagePh = result.phases.find((p) => p.phase === "page"); expect(pagePh?.resources.length).toBe(MOCK_PAGES.length);
const compPh = result.phases.find((p) => p.phase === "components"); expect(compPh?.resources.length).toBe(MOCK_COMPONENTS.length);
const groupPh = result.phases.find((p) => p.phase === "componentGroups"); expect(groupPh?.resources.length).toBe(MOCK_COMPONENT_GROUPS.length);
// Verify no pages were created in DB with the slug from mock const dbPage = await db .select() .from(page) .where(eq(page.slug, "acmecorp")) .get(); expect(dbPage).toBeUndefined();});
test("run creates page, components, and groups in DB", async () => { const result = await caller.import.run({ provider: "statuspage", apiKey: "test-key", options: { includeStatusReports: true, includeSubscribers: false }, });
trackCreatedIds(result);
// Overall status should be completed (subscribers skipped but that's fine) expect(["completed", "partial"]).toContain(result.status);
// Page should be created with slug "acmecorp" const dbPage = await db .select() .from(page) .where(eq(page.slug, "acmecorp")) .get(); if (!dbPage) throw new Error("Expected page to be created with slug 'acmecorp'"); expect(dbPage.title).toBe("Acme Corp Status"); expect(dbPage.workspaceId).toBe(TEST_WORKSPACE_ID);
// Components should be created const dbComponents = await db .select() .from(pageComponent) .where(eq(pageComponent.pageId, dbPage.id)) .all(); expect(dbComponents.length).toBe(MOCK_COMPONENTS.length);
// Groups should be created const dbGroups = await db .select() .from(pageComponentGroup) .where(eq(pageComponentGroup.pageId, dbPage.id)) .all(); expect(dbGroups.length).toBe(MOCK_COMPONENT_GROUPS.length);
// Subscribers phase should be skipped const subPhase = result.phases.find((p) => p.phase === "subscribers"); expect(subPhase?.status).toBe("skipped");
// Clean up for next tests await cleanup();});
test("run with existing pageId imports into that page", async () => { const result = await caller.import.run({ provider: "statuspage", apiKey: "test-key", pageId: TEST_PAGE_ID, options: { includeStatusReports: false, includeSubscribers: false }, });
trackCreatedIds(result);
expect(["completed", "partial"]).toContain(result.status);
// Page phase should show "skipped" since we used an existing page const pagePhase = result.phases.find((p) => p.phase === "page"); expect(pagePhase?.resources[0]?.status).toBe("skipped"); expect(pagePhase?.resources[0]?.openstatusId).toBe(TEST_PAGE_ID);
// Components should be created under page 1 const dbComponents = await db .select() .from(pageComponent) .where(eq(pageComponent.pageId, TEST_PAGE_ID)) .all(); // At least the components we imported should exist const importedCompNames = MOCK_COMPONENTS.map((c) => c.name); const matchingComps = dbComponents.filter((c) => importedCompNames.includes(c.name), ); expect(matchingComps.length).toBe(MOCK_COMPONENTS.length);
await cleanup();});
test("re-run skips already-imported resources (idempotency)", async () => { // First run const result1 = await caller.import.run({ provider: "statuspage", apiKey: "test-key", options: { includeStatusReports: false, includeSubscribers: false }, }); trackCreatedIds(result1);
// Second run (same data) const result2 = await caller.import.run({ provider: "statuspage", apiKey: "test-key", options: { includeStatusReports: false, includeSubscribers: false }, }); // Don't track again - same resources should be skipped
// Page should be skipped on second run const pagePhase = result2.phases.find((p) => p.phase === "page"); expect(pagePhase?.resources[0]?.status).toBe("skipped");
// Components should be skipped on second run const compPhase = result2.phases.find((p) => p.phase === "components"); const allSkipped = compPhase?.resources.every((r) => r.status === "skipped"); expect(allSkipped).toBe(true);
// Groups should be skipped on second run const groupPhase = result2.phases.find((p) => p.phase === "componentGroups"); const allGroupsSkipped = groupPhase?.resources.every( (r) => r.status === "skipped", ); expect(allGroupsSkipped).toBe(true);
await cleanup();});
test("run with includeStatusReports creates status reports", async () => { const result = await caller.import.run({ provider: "statuspage", apiKey: "test-key", options: { includeStatusReports: true, includeSubscribers: false }, });
trackCreatedIds(result);
// Incidents phase should be completed const incPhase = result.phases.find((p) => p.phase === "incidents"); expect(incPhase).toBeDefined(); expect(incPhase?.status).toBe("completed");
if (!incPhase) throw new Error("Expected incidents phase to exist");
// Verify status reports were created in DB const createdReportIds = incPhase.resources .filter((r) => r.status === "created" && r.openstatusId) .map((r) => r.openstatusId as number); expect(createdReportIds.length).toBeGreaterThan(0);
// Check that status report updates were created const updates = await db .select() .from(statusReportUpdate) .where(inArray(statusReportUpdate.statusReportId, createdReportIds)) .all(); expect(updates.length).toBeGreaterThan(0);
// Check that statusReportsToPageComponents links exist const links = await db .select() .from(statusReportsToPageComponents) .where( inArray(statusReportsToPageComponents.statusReportId, createdReportIds), ) .all(); expect(links.length).toBeGreaterThan(0);
// Maintenances phase should also have entries (scheduled incident) const maintPhase = result.phases.find((p) => p.phase === "maintenances"); if (!maintPhase) throw new Error("Expected maintenances phase to exist"); const createdMaintIds = maintPhase.resources .filter((r) => r.status === "created" && r.openstatusId) .map((r) => r.openstatusId as number); expect(createdMaintIds.length).toBeGreaterThan(0);
await cleanup();});
// ---------------------------------------------------------------------------// Limit warnings (go through the caller with custom workspace limits to// drive the service's plan gates)// ---------------------------------------------------------------------------
const freeLimits = { ...allPlans.free.limits };const starterLimits = { ...allPlans.starter.limits };
test("preview shows component limit warning on free plan", async () => { // Use workspaceId 2 (free seed) so the workspace-wide component count // starts at 0 โ workspace 1 has 2 seeded page components from the // shared seed, which would make `remaining = 1` instead of 3 and // change the warning wording. const c = makeCaller( { ...freeLimits, "page-components": 3 }, OTHER_WORKSPACE_ID, ); const result = await c.import.preview({ provider: "statuspage", apiKey: "test-key", });
expect(result.errors.length).toBeGreaterThan(0); // Warning was reworded to clarify the worst-case count (see // `limits.ts` โ dropped the `"X of Y"` fraction because it implied // a hard rejection count when duplicates would actually be skipped). expect(result.errors.some((e) => e.includes("3 new component"))).toBe(true);});
test("preview shows custom domain warning on free plan", async () => { const c = makeCaller({ ...freeLimits, "custom-domain": false }); const result = await c.import.preview({ provider: "statuspage", apiKey: "test-key", });
expect(result.errors.some((e) => e.includes("Custom domain"))).toBe(true);});
test("preview shows subscriber warning on free plan", async () => { const c = makeCaller({ ...freeLimits, "status-subscribers": false }); // Pass `options.includeSubscribers: true` explicitly โ the warning // fires only when the user *intends* to import subscribers (and the // plan disallows it). Default `includeSubscribers` is `false` // (matches `ImportOptions`), so without the override the preview // correctly stays silent for users who aren't importing subscribers. const result = await c.import.preview({ provider: "statuspage", apiKey: "test-key", options: { includeSubscribers: true }, });
expect(result.errors.some((e) => e.includes("Subscribers"))).toBe(true);});
test("preview shows no warnings on starter plan", async () => { const c = makeCaller(starterLimits); const result = await c.import.preview({ provider: "statuspage", apiKey: "test-key", });
// Only informational warnings about non-email subscribers are expected const limitWarnings = result.errors.filter( (e) => !e.includes("non-email subscriber"), ); expect(limitWarnings).toEqual([]);});
test("run enforces component limit by truncating", async () => { // Create a dedicated page so we control the state entirely const [testPage] = await db .insert(page) .values({ workspaceId: TEST_WORKSPACE_ID, title: "Truncation Test Page", description: "", slug: "truncation-test", customDomain: "", icon: "", }) .returning({ id: page.id });
if (!testPage) throw new Error("Failed to create test page"); createdIds.pages.push(testPage.id);
const c = makeCaller({ ...starterLimits, "page-components": 2 }); const result = await c.import.run({ provider: "statuspage", apiKey: "test-key", pageId: testPage.id, options: { includeStatusReports: false, includeSubscribers: false }, });
trackCreatedIds(result);
// Should have warning about component limit expect( result.errors.some( (e) => e.includes("page-components") || e.includes("Component limit") || e.includes("components can be imported") || e.includes("plan limit"), ), ).toBe(true);
// Components phase should show enforcement (either truncated or fully blocked) const compPhase = result.phases.find((p) => p.phase === "components"); expect(compPhase).toBeDefined(); // With limit of 2, not all 4 mock components should be created const created = compPhase?.resources.filter((r) => r.status === "created") ?? []; expect(created.length).toBeLessThanOrEqual(2);
await cleanup();});
test("run skips subscribers on free plan", async () => { const c = makeCaller({ ...freeLimits, "status-subscribers": false }); const result = await c.import.run({ provider: "statuspage", apiKey: "test-key", options: { includeStatusReports: false, includeSubscribers: true }, });
trackCreatedIds(result);
const subPhase = result.phases.find((p) => p.phase === "subscribers"); expect(subPhase?.status).toBe("skipped"); expect(result.errors.some((e) => e.includes("Subscribers"))).toBe(true);
await cleanup();});