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.
7.5 kB ยท 215 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216import "../test-preload.ts";import { EmailClient } from "@openstatus/emails";import { expect } from "@std/expect";import { afterEach, beforeEach, describe, test } from "@std/testing/bdd";import { assertSpyCalls, type Stub, stub } from "@std/testing/mock";
import type { PageUpdate, Subscription } from "../types";import { sendEmailNotifications, sendEmailVerification, validateEmailConfig,} from "./email";
// RESEND_API_KEY is set in test-preload.ts (see bunfig.toml) so @openstatus/emails// loads successfully and EmailClient prototype methods can be spied on.
let sendPageSubscriptionMock: Stub<EmailClient>;let sendStatusReportUpdateMock: Stub<EmailClient>;
function makeSub(overrides: Partial<Subscription> = {}): Subscription { return { id: 1, pageId: 1, pageName: "Test Page", pageSlug: "test", channelType: "email", email: "user@example.com", token: "token-123", componentIds: [], ...overrides, };}
function makeUpdate(overrides: Partial<PageUpdate> = {}): PageUpdate { return { id: 1, pageId: 1, title: "Test Incident", status: "investigating", message: "We are investigating.", pageComponentIds: [], pageComponents: [], date: new Date().toISOString(), ...overrides, };}
beforeEach(() => { sendPageSubscriptionMock = stub( EmailClient.prototype, "sendPageSubscription", () => Promise.resolve(undefined), ); sendStatusReportUpdateMock = stub( EmailClient.prototype, "sendStatusReportUpdate", () => Promise.resolve(undefined), );});
afterEach(() => { sendPageSubscriptionMock.restore(); sendStatusReportUpdateMock.restore();});
// โโโ validateEmailConfig โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
describe("validateEmailConfig", () => { test("returns valid for a valid email address", async () => { const result = await validateEmailConfig("user@example.com"); expect(result.valid).toBe(true); });
test("returns invalid for a plain string without @", async () => { const result = await validateEmailConfig("not-an-email"); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); });
test("returns invalid for non-string input", async () => { const result = await validateEmailConfig(42); expect(result.valid).toBe(false); });
test("returns invalid for null", async () => { const result = await validateEmailConfig(null); expect(result.valid).toBe(false); });});
// โโโ sendEmailVerification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
describe("sendEmailVerification", () => { test("throws when subscription has no email", async () => { const sub = makeSub({ email: undefined }); await expect( sendEmailVerification(sub, "https://example.com/verify/token"), ).rejects.toThrow("Email is required"); });
test("calls sendPageSubscription with the correct arguments", async () => { const sub = makeSub({ email: "user@example.com", pageName: "My Page" }); await sendEmailVerification(sub, "https://example.com/verify/abc");
assertSpyCalls(sendPageSubscriptionMock, 1); const [args] = sendPageSubscriptionMock.calls[0].args; expect(args.to).toBe("user@example.com"); expect(args.link).toBe("https://example.com/verify/abc"); expect(args.page).toBe("My Page"); });});
// โโโ sendEmailNotifications โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
describe("sendEmailNotifications", () => { test("does nothing for an empty subscriptions array", async () => { await sendEmailNotifications([], makeUpdate()); assertSpyCalls(sendStatusReportUpdateMock, 0); });
test("filters out subscriptions without an email address", async () => { const sub = makeSub({ email: undefined }); await sendEmailNotifications([sub], makeUpdate()); assertSpyCalls(sendStatusReportUpdateMock, 0); });
test("calls sendStatusReportUpdate once with all valid subscribers", async () => { const sub1 = makeSub({ email: "a@example.com", token: "token-a" }); const sub2 = makeSub({ email: "b@example.com", token: "token-b" }); const update = makeUpdate({ title: "Outage", status: "resolved" });
await sendEmailNotifications([sub1, sub2], update);
assertSpyCalls(sendStatusReportUpdateMock, 1); const [args] = sendStatusReportUpdateMock.calls[0].args; expect(args.subscribers).toHaveLength(2); expect(args.subscribers[0].email).toBe("a@example.com"); expect(args.subscribers[1].email).toBe("b@example.com"); expect(args.reportTitle).toBe("Outage"); expect(args.status).toBe("resolved"); });
test("passes page components to sendStatusReportUpdate", async () => { const sub = makeSub({ email: "user@example.com" }); const update = makeUpdate({ pageComponents: ["API", "Database"] });
await sendEmailNotifications([sub], update);
const [args] = sendStatusReportUpdateMock.calls[0].args; expect(args.pageComponents).toEqual(["API", "Database"]); });
test("derives the idempotency key from the status-report update id", async () => { const sub = makeSub(); await sendEmailNotifications([sub], makeUpdate({ updateId: 77 }));
const [args] = sendStatusReportUpdateMock.calls[0].args; expect(args.idempotencyKey).toMatch(/^status-report-update:77:[0-9a-z]+$/); });
test("falls back to a page-update key when there is no update id (maintenance)", async () => { const sub = makeSub(); await sendEmailNotifications( [sub], makeUpdate({ id: 17, updateId: undefined, status: "maintenance" }), );
const [args] = sendStatusReportUpdateMock.calls[0].args; expect(args.idempotencyKey).toMatch( /^page-update:17:maintenance:[0-9a-z]+$/, ); });
test("keeps the same key for an identical re-dispatch", async () => { const update = makeUpdate({ updateId: 77, date: "2026-07-16T11:00:00Z" }); await sendEmailNotifications([makeSub()], update); await sendEmailNotifications([makeSub()], update);
const keys = sendStatusReportUpdateMock.calls.map( (c) => c.args[0].idempotencyKey, ); expect(keys[0]).toBe(keys[1]); });
test("changes the key when the subscriber list changes", async () => { const update = makeUpdate({ updateId: 77, date: "2026-07-16T11:00:00Z" }); await sendEmailNotifications([makeSub()], update); await sendEmailNotifications( [makeSub(), makeSub({ id: 2, email: "b@example.com", token: "token-b" })], update, );
const keys = sendStatusReportUpdateMock.calls.map( (c) => c.args[0].idempotencyKey, ); expect(keys[0]).not.toBe(keys[1]); });
test("changes the key when the update content changes", async () => { const date = "2026-07-16T11:00:00Z"; await sendEmailNotifications( [makeSub()], makeUpdate({ updateId: 77, date, message: "We are investigating." }), ); await sendEmailNotifications( [makeSub()], makeUpdate({ updateId: 77, date, message: "Root cause identified." }), );
const keys = sendStatusReportUpdateMock.calls.map( (c) => c.args[0].idempotencyKey, ); expect(keys[0]).not.toBe(keys[1]); });});