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.
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214import { and, eq, sql } from "@openstatus/db";import { auditLog, incidentEvent } from "@openstatus/db/src/schema";import { addUserToWorkspace, createUser,} from "@openstatus/db/src/test/factories";import { expect } from "@std/expect";import { beforeAll, describe, test } from "@std/testing/bdd";
import { createWorkspaceFixture, expectAuditRow, makeUserCtx, withTestTransaction,} from "../../../test/helpers";import type { DB, ServiceContext } from "../../context";import { ConflictError, ForbiddenError } from "../../errors";import type { Workspace } from "../../types";import { approvePostmortem, closeIncident, declareIncident, draftPostmortem, getIncident, getPostmortem, setIncidentStatus,} from "../index";
let workspace: Workspace;let ownerId: number;let memberId: number;
beforeAll(async () => { const fixture = await createWorkspaceFixture("team"); workspace = fixture.workspace; ownerId = fixture.userId; memberId = (await createUser()).id; await addUserToWorkspace(memberId, workspace.id, "member");});
const as = (userId: number, tx: DB): ServiceContext => ({ ...makeUserCtx(workspace, { userId }), db: tx,});
async function resolved(tx: DB) { const row = await declareIncident({ ctx: as(memberId, tx), input: { title: "API down", severity: "major", commanderId: ownerId }, }); await setIncidentStatus({ ctx: as(memberId, tx), input: { id: row.id, status: "resolved" }, }); return row;}
async function eventTypes(tx: DB, incidentId: number) { const rows = await tx .select({ type: incidentEvent.type }) .from(incidentEvent) .where(eq(incidentEvent.incidentId, incidentId)) .all(); return rows.map((r) => r.type);}
async function eventAudits(tx: DB, incidentId: number) { const rows = await tx .select({ id: auditLog.id }) .from(auditLog) .where( and( eq(auditLog.action, "incident_event.create"), sql`json_extract(${auditLog.metadata}, '$.incidentId') = ${incidentId}`, ), ) .all(); return rows.length;}
describe("postmortem", () => { test("draft, redraft, approve and close; every event audited", async () => { await withTestTransaction(async (tx) => { const row = await resolved(tx); const draft = await draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "## Summary\n\nFirst", draftedBy: "agent", sourceTranscript: "U1: it's down", }, }); expect(draft.status).toBe("draft"); expect(draft.createdBy).toBe(memberId); await expectAuditRow({ workspaceId: workspace.id, action: "incident_postmortem.create", entityType: "incident_postmortem", entityId: draft.id, db: tx, });
const redraft = await draftPostmortem({ ctx: as(ownerId, tx), input: { id: row.id, content: "## Summary\n\nSecond" }, }); expect(redraft.id).toBe(draft.id); expect(redraft.sourceTranscript).toBe("U1: it's down"); expect(redraft.updatedBy).toBe(ownerId);
await expect( closeIncident({ ctx: as(ownerId, tx), input: { id: row.id } }), ).rejects.toThrow(ConflictError);
const approved = await approvePostmortem({ ctx: as(ownerId, tx), input: { id: row.id, close: true }, }); expect(approved.status).toBe("approved"); expect(approved.approvedBy).toBe(ownerId); expect( (await getIncident({ ctx: as(ownerId, tx), input: { id: row.id } })) ?.closedAt, ).not.toBeNull();
const types = await eventTypes(tx, row.id); expect(types).toContain("postmortem_drafted"); expect(types).toContain("postmortem_approved"); expect(types).toContain("closed"); expect(await eventAudits(tx, row.id)).toBe(types.length); }); });
test("editing an approved postmortem keeps it approved; the agent can't redraft it", async () => { await withTestTransaction(async (tx) => { const row = await resolved(tx); await draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "v1" }, }); await approvePostmortem({ ctx: as(ownerId, tx), input: { id: row.id }, }); const edited = await draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "v2" }, }); expect(edited.status).toBe("approved"); expect(edited.content).toBe("v2"); expect(await eventTypes(tx, row.id)).toContain("postmortem_updated"); await expect( draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "v3", draftedBy: "agent" }, }), ).rejects.toThrow(ConflictError); expect( (await getPostmortem({ ctx: as(memberId, tx), input: { id: row.id } })) ?.content, ).toBe("v2"); }); });
test("only admin, owner or the commander approves", async () => { await withTestTransaction(async (tx) => { const row = await resolved(tx); await draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "v1" }, }); await expect( approvePostmortem({ ctx: as(memberId, tx), input: { id: row.id } }), ).rejects.toThrow(ForbiddenError); }); });
test("a reopened incident can't have its postmortem approved", async () => { await withTestTransaction(async (tx) => { const row = await resolved(tx); await draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "v1" }, }); await setIncidentStatus({ ctx: as(memberId, tx), input: { id: row.id, status: "open" }, }); await expect( approvePostmortem({ ctx: as(ownerId, tx), input: { id: row.id, close: false }, }), ).rejects.toThrow(ConflictError); }); });
test("an unresolved incident has no postmortem yet", async () => { await withTestTransaction(async (tx) => { const row = await declareIncident({ ctx: as(memberId, tx), input: { title: "x", severity: "minor" }, }); await expect( draftPostmortem({ ctx: as(memberId, tx), input: { id: row.id, content: "too early" }, }), ).rejects.toThrow(ConflictError); }); });});