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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413import { sentry } from "@hono/sentry";import { Events } from "@openstatus/analytics";import { db, desc, eq } from "@openstatus/db";import { auditLog, page, statusReport } from "@openstatus/db/src/schema";import { resourceMetadataUrl } from "@openstatus/services/oauth";import { SEEDED_WORKSPACE_TEAM_ID } from "@openstatus/services/test/fixtures";import type { MockFn } from "@openstatus/test-utils";import { expect } from "@std/expect";import { afterAll, beforeAll, beforeEach, describe, test,} from "@std/testing/bdd";import { Hono } from "hono";import { requestId } from "hono/request-id";
import { handleError } from "../../libs/errors";import { oauthConfigFromEnv } from "../oauth/config";import { mcpRoute } from "./index";
/** * Build a fresh Hono app with the same middleware chain as production. * `@hono/sentry` is mounted even without a DSN โ `handleError` reads * `c.get("sentry")` on client errors (status < 499) and would silently * fail without it, masking 401s as 200s. */function makeApp() { const app = new Hono<{ Variables: { event: Record<string, unknown> } }>(); app.use("*", sentry({ dsn: undefined })); app.use("*", requestId()); app.use("*", async (c, next) => { // The auth middleware consults `c.get("event")` to record auth_method. c.set("event", {}); await next(); }); app.onError(handleError); app.route("/mcp", mcpRoute); return app;}
const KEY = String(SEEDED_WORKSPACE_TEAM_ID);
/** * Build a JSON-RPC POST request. Pass `key: false` to omit the * `x-openstatus-key` header entirely (testing the unauthenticated * path) โ `undefined` would trigger the parameter default and * silently authenticate, masking the missing-auth case. */function jsonRpc(body: object, key: string | false = KEY): Request { const headers: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json, text/event-stream", }; if (key !== false) headers["x-openstatus-key"] = key; return new Request("http://localhost/mcp", { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, ...body }), });}
async function readJsonRpc( res: Response,): Promise<{ result?: unknown; error?: { code: number; message: string } }> { const ct = res.headers.get("content-type") ?? ""; if (ct.includes("text/event-stream")) { const text = await res.text(); // SSE frames look like `event: message\ndata: {...}\n\n`. Pull the // first `data:` payload. const match = text.match(/data:\s*({.*})/); if (!match) throw new Error(`no data frame in SSE response: ${text}`); return JSON.parse(match[1]); } return res.json() as Promise<{ result?: unknown; error?: { code: number; message: string }; }>;}
describe("MCP transport", () => { test("a request without a key is a 401 that starts OAuth discovery", async () => { const app = makeApp(); const res = await app.fetch(jsonRpc({ method: "initialize" }, false)); expect(res.status).toBe(401); // RFC 9728: the challenge points at the protected-resource metadata, which // is how an MCP client finds the authorization server. Without it the // client has a dead endpoint rather than a protected one. const challenge = res.headers.get("WWW-Authenticate") ?? ""; expect(challenge.startsWith("Bearer ")).toBe(true); expect(challenge).toContain( `resource_metadata="${resourceMetadataUrl(oauthConfigFromEnv().issuer)}"`, ); });
test("a present but empty key is a 401 too", async () => { const app = makeApp(); const res = await app.fetch(jsonRpc({ method: "resources/list" }, "")); expect(res.status).toBe(401); expect(res.headers.get("WWW-Authenticate")).toBeTruthy(); });
test("resources/list serves the public documents to an authenticated key", async () => { const app = makeApp(); const res = await app.fetch(jsonRpc({ method: "resources/list" })); expect(res.status).toBe(200); const body = await readJsonRpc(res); const resources = (body.result as { resources: { uri: string }[] }) .resources; expect(resources.length).toBeGreaterThan(0); });
test("tools/list returns the expected registered tool set", async () => { const app = makeApp(); const res = await app.fetch(jsonRpc({ method: "tools/list" })); expect(res.status).toBe(200); const body = await readJsonRpc(res); const tools = (body.result as { tools: { name: string }[] }).tools; const names = tools.map((t) => t.name).sort(); expect(names).toEqual([ "add_incident_note", "add_status_report_update", "approve_postmortem", "create_maintenance", "create_status_report", "declare_incident", "get_audit_log", "get_content_page", "get_doc_page", "get_incident", "get_monitor", "get_monitor_status", "get_monitor_summary", "get_postmortem", "get_response_log", "list_audit_logs", "list_incidents", "list_maintenances", "list_monitors", "list_notifications", "list_page_components", "list_private_locations", "list_response_logs", "list_status_pages", "list_status_reports", "resolve_incident", "resolve_status_report", "search_content", "search_docs", "set_incident_status", "update_incident", "update_status_report", ]); });
test("tools/call list_status_pages succeeds with valid auth", async () => { const app = makeApp(); const res = await app.fetch( jsonRpc({ method: "tools/call", params: { name: "list_status_pages", arguments: {} }, }), ); expect(res.status).toBe(200); const body = await readJsonRpc(res); expect(body.error).toBeUndefined(); const result = body.result as { structuredContent?: { items: unknown[] }; isError?: boolean; }; expect(result.isError).toBeUndefined(); expect(Array.isArray(result.structuredContent?.items)).toBe(true); });
test("tools/call with unknown tool name surfaces an error", async () => { const app = makeApp(); const res = await app.fetch( jsonRpc({ method: "tools/call", params: { name: "definitely_not_a_tool", arguments: {} }, }), ); // The SDK can surface unknown-tool failures three ways: as a // JSON-RPC error, an HTTP 4xx, or as a `tools/call` result with // `isError: true`. Accept any โ assert "not silently successful". if (res.status === 200) { const body = (await readJsonRpc(res)) as { result?: { isError?: boolean }; error?: { code: number; message: string }; }; const failed = body.error !== undefined || body.result?.isError === true; expect(failed).toBe(true); } else { expect(res.status).toBeGreaterThanOrEqual(400); } });
test("update_status_report rejects status: 'resolved' via Zod refine", async () => { const app = makeApp(); // We don't need a real status report โ input validation runs // against the schema before the handler executes, so the SDK // surfaces the refine error regardless of statusReportId. const res = await app.fetch( jsonRpc({ method: "tools/call", params: { name: "update_status_report", arguments: { statusReportId: 1, status: "resolved" }, }, }), ); expect(res.status).toBe(200); const body = (await readJsonRpc(res)) as { result?: { isError?: boolean; content?: { text: string }[] }; error?: { code: number; message: string }; }; const failed = body.error !== undefined || body.result?.isError === true; expect(failed).toBe(true); const text = body.error?.message ?? body.result?.content?.[0]?.text ?? ""; expect(text).toMatch(/resolve_status_report/); });
test("tools/list advertises non-empty descriptions and schemas", async () => { const app = makeApp(); const res = await app.fetch(jsonRpc({ method: "tools/list" })); const body = await readJsonRpc(res); const tools = ( body.result as { tools: { name: string; description?: string; inputSchema?: object; outputSchema?: object; }[]; } ).tools; expect(tools.length).toBeGreaterThan(0); for (const tool of tools) { expect(tool.description?.length ?? 0).toBeGreaterThan(40); expect(tool.inputSchema).toBeDefined(); expect(tool.outputSchema).toBeDefined(); } });});
/** * `@openstatus/analytics` is swapped for a double (test.importmap.json) whose * spies live on `globalThis.__analyticsSpies`. */const analyticsSpies = (globalThis as Record<string, unknown>) .__analyticsSpies as { track: MockFn; setupAnalytics: MockFn };
/** * Tracking is fire-and-forget โ `track` runs in a microtask chained off * `setupAnalytics`, which may not have settled when `app.fetch` resolves. */async function flushTracking() { await new Promise((resolve) => setTimeout(resolve, 0));}
describe("MCP transport โ analytics", () => { beforeEach(() => { analyticsSpies.setupAnalytics.mockClear(); analyticsSpies.track.mockClear(); });
test("tools/call tracks mcp_request with the tool name", async () => { const app = makeApp(); await app.fetch( jsonRpc({ method: "tools/call", params: { name: "list_status_pages", arguments: {} }, }), ); await flushTracking();
const identify = analyticsSpies.setupAnalytics.mock.calls[0]?.[0] as Record< string, unknown >; expect(identify?.userId).toBe(`api_${SEEDED_WORKSPACE_TEAM_ID}`);
const event = analyticsSpies.track.mock.calls[0]?.[0] as Record< string, unknown >; expect(event?.name).toBe(Events.McpRequest.name); expect(event?.method).toBe("tools/call"); expect(event?.tool).toBe("list_status_pages"); expect(event?.authenticated).toBe(true); });
test("a rejected request is not tracked", async () => { const app = makeApp(); await app.fetch(jsonRpc({ method: "resources/list" }, false)); await flushTracking();
// `authMiddleware` throws before the handler runs, so unauthenticated // traffic never reaches the tracker: the event counts real MCP calls. expect(analyticsSpies.track.mock.calls.length).toBe(0); });});
/** * End-to-end actor stamping โ drive a real `tools/call` through * `app.fetch` and verify the resulting audit row has * `actorType: "mcp"` and a real `actorId` (the API key id captured by * the auth middleware, not a workspace placeholder). Distinct from the * unit tests, which assert this on direct handler calls โ this proves * the entire chain (auth โ toServiceCtx โ tool โ service โ emitAudit) * end-to-end. * * Cannot use `withTestTransaction` here because the request goes * through `app.fetch` which reaches the default db. Tracks created ids * for `afterAll` cleanup. */describe("MCP transport โ audit stamping", () => { const E2E_PREFIX = "mcp-handler-test"; const E2E_SLUG_SUFFIX = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; let e2ePageId: number | null = null; const createdReports: number[] = [];
beforeAll(async () => { const row = await db .insert(page) .values({ workspaceId: SEEDED_WORKSPACE_TEAM_ID, title: `${E2E_PREFIX}-page`, description: "e2e page", slug: `${E2E_PREFIX}-page-slug-${E2E_SLUG_SUFFIX}`, customDomain: "", }) .returning() .get(); e2ePageId = row.id; });
afterAll(async () => { for (const id of createdReports) { await db .delete(auditLog) .where(eq(auditLog.entityId, String(id))) .catch(() => undefined); await db .delete(statusReport) .where(eq(statusReport.id, id)) .catch(() => undefined); } if (e2ePageId !== null) { await db .delete(page) .where(eq(page.id, e2ePageId)) .catch(() => undefined); } });
test("tools/call create_status_report writes audit row with transport=mcp", async () => { const app = makeApp(); if (e2ePageId === null) throw new Error("setup failed"); const res = await app.fetch( jsonRpc({ method: "tools/call", params: { name: "create_status_report", arguments: { title: `${E2E_PREFIX}-create`, status: "investigating", message: "e2e investigating", pageId: e2ePageId, pageComponentIds: [], notify: false, }, }, }), ); expect(res.status).toBe(200); const body = await readJsonRpc(res); expect(body.error).toBeUndefined(); const result = body.result as { isError?: boolean; structuredContent?: { statusReport: { id: number }; }; }; expect(result.isError).toBeUndefined(); const reportId = result.structuredContent?.statusReport.id; expect(reportId).toBeGreaterThan(0); if (typeof reportId === "number") createdReports.push(reportId);
const rows = await db .select() .from(auditLog) .where(eq(auditLog.entityId, String(reportId))) .orderBy(desc(auditLog.id)) .all(); const createRow = rows.find((r) => r.action === "status_report.create"); expect(createRow).toBeDefined(); expect(createRow?.actorType).toBe("mcp"); // In dev mode `validateKey` short-circuits the custom-key DB // lookup and treats the input string as both ownerId and keyId. // Asserting actorId === KEY proves keyId propagates structurally // through auth โ adapter โ service โ audit, but does NOT exercise // the production `api_key.id` path. A NODE_ENV=production test // with a real api_key fixture would close that gap; out of scope // here because flipping NODE_ENV reroutes other middleware. expect(createRow?.actorId).toBe(KEY); // No `createdById` available in dev mode โ audit's actorUserId // is null. Production custom keys would carry the creator's id. expect(createRow?.actorUserId).toBeNull(); });});