From d8388829500bb58666d0c7907b6976c8781e2131 Mon Sep 17 00:00:00 2001 From: Jonathan Barazany Date: Sun, 8 Mar 2026 19:18:39 +0200 Subject: [PATCH 1/3] fix: resolve flaky tests caused by shared state between parallel test files Tests running in parallel via bun test share a single database, causing race conditions when RPC tests create temporary data in workspace 2 while V1 tests assert workspace 2 is empty. Two fixes applied: 1. Move feature-specific permission checks (password-protection, email-domain-protection) before the page count check in the page creation route. This ensures the correct 402 error message is returned regardless of how many pages exist for the workspace. 2. Switch "return empty" tests to use workspace 3, which is not used by any RPC test for cross-workspace validation, eliminating the race condition with concurrent test files. Closes #1928 --- .../src/routes/v1/incidents/get_all.test.ts | 2 +- .../routes/v1/maintenances/get_all.test.ts | 2 +- .../routes/v1/notifications/get_all.test.ts | 2 +- .../src/routes/v1/pages/get_all.test.ts | 2 +- apps/server/src/routes/v1/pages/post.ts | 30 +++++++++---------- .../routes/v1/statusReports/get_all.test.ts | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/server/src/routes/v1/incidents/get_all.test.ts b/apps/server/src/routes/v1/incidents/get_all.test.ts index 77de0cd441..0d7f32c39d 100644 --- a/apps/server/src/routes/v1/incidents/get_all.test.ts +++ b/apps/server/src/routes/v1/incidents/get_all.test.ts @@ -22,7 +22,7 @@ test("return empty incidents", async () => { const res = await app.request("/v1/incident", { method: "GET", headers: { - "x-openstatus-key": "2", + "x-openstatus-key": "3", }, }); diff --git a/apps/server/src/routes/v1/maintenances/get_all.test.ts b/apps/server/src/routes/v1/maintenances/get_all.test.ts index c905b5d59e..6183420ec5 100644 --- a/apps/server/src/routes/v1/maintenances/get_all.test.ts +++ b/apps/server/src/routes/v1/maintenances/get_all.test.ts @@ -41,7 +41,7 @@ test("return empty maintenances", async () => { const res = await app.request("/v1/maintenance", { method: "GET", headers: { - "x-openstatus-key": "2", + "x-openstatus-key": "3", }, }); diff --git a/apps/server/src/routes/v1/notifications/get_all.test.ts b/apps/server/src/routes/v1/notifications/get_all.test.ts index fd584c8729..c634adb402 100644 --- a/apps/server/src/routes/v1/notifications/get_all.test.ts +++ b/apps/server/src/routes/v1/notifications/get_all.test.ts @@ -22,7 +22,7 @@ test("return empty notifications", async () => { const res = await app.request("/v1/notification", { method: "GET", headers: { - "x-openstatus-key": "2", + "x-openstatus-key": "3", }, }); diff --git a/apps/server/src/routes/v1/pages/get_all.test.ts b/apps/server/src/routes/v1/pages/get_all.test.ts index 836e0bbd12..79a632ffc5 100644 --- a/apps/server/src/routes/v1/pages/get_all.test.ts +++ b/apps/server/src/routes/v1/pages/get_all.test.ts @@ -22,7 +22,7 @@ test("return empty pages", async () => { const res = await app.request("/v1/page", { method: "GET", headers: { - "x-openstatus-key": "2", + "x-openstatus-key": "3", }, }); diff --git a/apps/server/src/routes/v1/pages/post.ts b/apps/server/src/routes/v1/pages/post.ts index 4ba09dc5ef..c4e01cb78a 100644 --- a/apps/server/src/routes/v1/pages/post.ts +++ b/apps/server/src/routes/v1/pages/post.ts @@ -65,21 +65,6 @@ export function registerPostPage(api: typeof pagesApi) { }); } - const count = ( - await db - .select({ count: sql`count(*)` }) - .from(page) - .where(eq(page.workspaceId, workspaceId)) - .all() - )[0].count; - - if (count >= limits["status-pages"]) { - throw new OpenStatusApiError({ - code: "PAYMENT_REQUIRED", - message: "Upgrade for more status pages", - }); - } - if ( !limits["password-protection"] && (input?.passwordProtected || input?.password) @@ -110,6 +95,21 @@ export function registerPostPage(api: typeof pagesApi) { }); } + const count = ( + await db + .select({ count: sql`count(*)` }) + .from(page) + .where(eq(page.workspaceId, workspaceId)) + .all() + )[0].count; + + if (count >= limits["status-pages"]) { + throw new OpenStatusApiError({ + code: "PAYMENT_REQUIRED", + message: "Upgrade for more status pages", + }); + } + if (subdomainSafeList.includes(input.slug)) { throw new OpenStatusApiError({ code: "BAD_REQUEST", diff --git a/apps/server/src/routes/v1/statusReports/get_all.test.ts b/apps/server/src/routes/v1/statusReports/get_all.test.ts index 77c81e8018..ab465afb31 100644 --- a/apps/server/src/routes/v1/statusReports/get_all.test.ts +++ b/apps/server/src/routes/v1/statusReports/get_all.test.ts @@ -46,7 +46,7 @@ test("return empty status reports", async () => { const res = await app.request("/v1/status_report", { method: "GET", headers: { - "x-openstatus-key": "2", + "x-openstatus-key": "3", }, }); From f3e92b817734dfe1a5f68397c16aaf2c00c4f313 Mon Sep 17 00:00:00 2001 From: Jonathan Barazany Date: Sat, 14 Mar 2026 17:51:11 +0200 Subject: [PATCH 2/3] fix: use beforeAll/afterAll for test data setup instead of separate workspace IDs Refactor get_all test files to create their own test data in beforeAll and clean up in afterAll, following the pattern from monitor.test.ts. Each file uses a unique TEST_PREFIX for isolation during parallel runs. Use workspace 3 for empty tests since other tests write to workspace 2. --- .../src/routes/v1/incidents/get_all.test.ts | 54 ++++++++- .../routes/v1/maintenances/get_all.test.ts | 104 ++++++++++++++-- .../routes/v1/notifications/get_all.test.ts | 33 ++++- .../src/routes/v1/pages/get_all.test.ts | 65 +++++++++- .../routes/v1/statusReports/get_all.test.ts | 114 ++++++++++++++++-- 5 files changed, 343 insertions(+), 27 deletions(-) diff --git a/apps/server/src/routes/v1/incidents/get_all.test.ts b/apps/server/src/routes/v1/incidents/get_all.test.ts index 0d7f32c39d..1d298e8d6a 100644 --- a/apps/server/src/routes/v1/incidents/get_all.test.ts +++ b/apps/server/src/routes/v1/incidents/get_all.test.ts @@ -1,8 +1,58 @@ -import { expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { db, eq } from "@openstatus/db"; +import { incidentTable, monitor } from "@openstatus/db/src/schema"; import { app } from "@/index"; import { IncidentSchema } from "./schema"; +const TEST_PREFIX = "v1-incident-getall-test"; +let testMonitorId: number; +let testIncidentId: number; + +beforeAll(async () => { + await db + .delete(incidentTable) + .where(eq(incidentTable.title, `${TEST_PREFIX}-incident`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); + + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-monitor`, + url: "https://test.example.com", + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + method: "GET", + timeout: 30000, + }) + .returning() + .get(); + testMonitorId = mon.id; + + const incident = await db + .insert(incidentTable) + .values({ + workspaceId: 1, + monitorId: testMonitorId, + title: `${TEST_PREFIX}-incident`, + status: "investigating", + startedAt: new Date("2099-01-01T00:00:00Z"), + }) + .returning() + .get(); + testIncidentId = incident.id; +}); + +afterAll(async () => { + await db + .delete(incidentTable) + .where(eq(incidentTable.title, `${TEST_PREFIX}-incident`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); +}); + test("return all incidents", async () => { const res = await app.request("/v1/incident", { method: "GET", @@ -15,7 +65,7 @@ test("return all incidents", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); + expect(result.data?.some((i) => i.id === testIncidentId)).toBe(true); }); test("return empty incidents", async () => { diff --git a/apps/server/src/routes/v1/maintenances/get_all.test.ts b/apps/server/src/routes/v1/maintenances/get_all.test.ts index 6183420ec5..2841437c06 100644 --- a/apps/server/src/routes/v1/maintenances/get_all.test.ts +++ b/apps/server/src/routes/v1/maintenances/get_all.test.ts @@ -1,7 +1,93 @@ -import { expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { db, eq } from "@openstatus/db"; +import { + maintenance, + maintenancesToPageComponents, + monitor, + pageComponent, +} from "@openstatus/db/src/schema"; + import { app } from "@/index"; import { MaintenanceSchema } from "./schema"; +const TEST_PREFIX = "v1-maint-getall-test"; +let testMonitorId: number; +let testPageComponentId: number; +let testMaintenanceId: number; + +beforeAll(async () => { + await db + .delete(maintenance) + .where(eq(maintenance.title, `${TEST_PREFIX}-maint`)); + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); + + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-monitor`, + url: "https://test.example.com", + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + method: "GET", + timeout: 30000, + }) + .returning() + .get(); + testMonitorId = mon.id; + + const comp = await db + .insert(pageComponent) + .values({ + workspaceId: 1, + pageId: 1, + monitorId: testMonitorId, + type: "monitor", + name: `${TEST_PREFIX}-component`, + order: 200, + }) + .returning() + .get(); + testPageComponentId = comp.id; + + const maint = await db + .insert(maintenance) + .values({ + workspaceId: 1, + pageId: 1, + title: `${TEST_PREFIX}-maint`, + message: "Test maintenance", + from: new Date("2099-01-01T00:00:00Z"), + to: new Date("2099-01-02T00:00:00Z"), + }) + .returning() + .get(); + testMaintenanceId = maint.id; + + await db.insert(maintenancesToPageComponents).values({ + maintenanceId: testMaintenanceId, + pageComponentId: testPageComponentId, + }); +}); + +afterAll(async () => { + await db + .delete(maintenancesToPageComponents) + .where(eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId)); + await db + .delete(maintenance) + .where(eq(maintenance.title, `${TEST_PREFIX}-maint`)); + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); +}); + test("return all maintenances", async () => { const res = await app.request("/v1/maintenance", { method: "GET", @@ -11,10 +97,10 @@ test("return all maintenances", async () => { }); const result = MaintenanceSchema.array().safeParse(await res.json()); - console.log(result); + expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); + expect(result.data?.some((m) => m.id === testMaintenanceId)).toBe(true); }); test("return all maintenances with monitorIds", async () => { @@ -29,12 +115,12 @@ test("return all maintenances with monitorIds", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); - // Each maintenance should have monitorIds defined - for (const maintenance of result.data || []) { - expect(maintenance.monitorIds).toBeDefined(); - expect(Array.isArray(maintenance.monitorIds)).toBe(true); - } + + const testMaint = result.data?.find((m) => m.id === testMaintenanceId); + expect(testMaint).toBeDefined(); + expect(testMaint?.monitorIds).toBeDefined(); + expect(Array.isArray(testMaint?.monitorIds)).toBe(true); + expect(testMaint?.monitorIds).toContain(testMonitorId); }); test("return empty maintenances", async () => { diff --git a/apps/server/src/routes/v1/notifications/get_all.test.ts b/apps/server/src/routes/v1/notifications/get_all.test.ts index c634adb402..4b88ec4312 100644 --- a/apps/server/src/routes/v1/notifications/get_all.test.ts +++ b/apps/server/src/routes/v1/notifications/get_all.test.ts @@ -1,8 +1,37 @@ -import { expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { db, eq } from "@openstatus/db"; +import { notification } from "@openstatus/db/src/schema"; import { app } from "@/index"; import { NotificationSchema } from "./schema"; +const TEST_PREFIX = "v1-notif-getall-test"; +let testNotificationId: number; + +beforeAll(async () => { + await db + .delete(notification) + .where(eq(notification.name, `${TEST_PREFIX}-email`)); + + const notif = await db + .insert(notification) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-email`, + provider: "email", + data: '{"email":"test@test.com"}', + }) + .returning() + .get(); + testNotificationId = notif.id; +}); + +afterAll(async () => { + await db + .delete(notification) + .where(eq(notification.name, `${TEST_PREFIX}-email`)); +}); + test("return all notifications", async () => { const res = await app.request("/v1/notification", { method: "GET", @@ -15,7 +44,7 @@ test("return all notifications", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); + expect(result.data?.some((n) => n.id === testNotificationId)).toBe(true); }); test("return empty notifications", async () => { diff --git a/apps/server/src/routes/v1/pages/get_all.test.ts b/apps/server/src/routes/v1/pages/get_all.test.ts index 79a632ffc5..8427558229 100644 --- a/apps/server/src/routes/v1/pages/get_all.test.ts +++ b/apps/server/src/routes/v1/pages/get_all.test.ts @@ -1,8 +1,69 @@ -import { expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { db, eq } from "@openstatus/db"; +import { monitor, page, pageComponent } from "@openstatus/db/src/schema"; import { app } from "@/index"; import { PageSchema } from "./schema"; +const TEST_PREFIX = "v1-page-getall-test"; +let testMonitorId: number; +let testPageId: number; + +beforeAll(async () => { + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); + + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-monitor`, + url: "https://test.example.com", + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + method: "GET", + timeout: 30000, + }) + .returning() + .get(); + testMonitorId = mon.id; + + const p = await db + .insert(page) + .values({ + workspaceId: 1, + title: `${TEST_PREFIX}-page`, + slug: `${TEST_PREFIX}-slug`, + description: "Test page", + customDomain: "", + }) + .returning() + .get(); + testPageId = p.id; + + await db.insert(pageComponent).values({ + workspaceId: 1, + pageId: testPageId, + monitorId: testMonitorId, + type: "monitor", + name: `${TEST_PREFIX}-component`, + order: 0, + }); +}); + +afterAll(async () => { + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); +}); + test("return all pages", async () => { const res = await app.request("/v1/page", { method: "GET", @@ -15,7 +76,7 @@ test("return all pages", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); + expect(result.data?.some((p) => p.id === testPageId)).toBe(true); }); test("return empty pages", async () => { diff --git a/apps/server/src/routes/v1/statusReports/get_all.test.ts b/apps/server/src/routes/v1/statusReports/get_all.test.ts index ab465afb31..7cc362dc06 100644 --- a/apps/server/src/routes/v1/statusReports/get_all.test.ts +++ b/apps/server/src/routes/v1/statusReports/get_all.test.ts @@ -1,8 +1,102 @@ -import { expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; +import { db, eq } from "@openstatus/db"; +import { + monitor, + pageComponent, + statusReport, + statusReportUpdate, + statusReportsToPageComponents, +} from "@openstatus/db/src/schema"; import { app } from "@/index"; import { StatusReportSchema } from "./schema"; +const TEST_PREFIX = "v1-sr-getall-test"; +let testMonitorId: number; +let testPageComponentId: number; +let testStatusReportId: number; + +beforeAll(async () => { + await db + .delete(statusReport) + .where(eq(statusReport.title, `${TEST_PREFIX}-report`)); + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); + + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-monitor`, + url: "https://test.example.com", + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + method: "GET", + timeout: 30000, + }) + .returning() + .get(); + testMonitorId = mon.id; + + const comp = await db + .insert(pageComponent) + .values({ + workspaceId: 1, + pageId: 1, + monitorId: testMonitorId, + type: "monitor", + name: `${TEST_PREFIX}-component`, + order: 200, + }) + .returning() + .get(); + testPageComponentId = comp.id; + + const report = await db + .insert(statusReport) + .values({ + workspaceId: 1, + pageId: 1, + title: `${TEST_PREFIX}-report`, + status: "investigating", + }) + .returning() + .get(); + testStatusReportId = report.id; + + await db.insert(statusReportUpdate).values({ + statusReportId: testStatusReportId, + status: "investigating", + message: "Test investigating", + date: new Date("2099-01-01T00:00:00Z"), + }); + + await db.insert(statusReportsToPageComponents).values({ + statusReportId: testStatusReportId, + pageComponentId: testPageComponentId, + }); +}); + +afterAll(async () => { + await db + .delete(statusReportsToPageComponents) + .where(eq(statusReportsToPageComponents.statusReportId, testStatusReportId)); + await db + .delete(statusReportUpdate) + .where(eq(statusReportUpdate.statusReportId, testStatusReportId)); + await db + .delete(statusReport) + .where(eq(statusReport.title, `${TEST_PREFIX}-report`)); + await db + .delete(pageComponent) + .where(eq(pageComponent.name, `${TEST_PREFIX}-component`)); + await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`)); +}); + test("return all status reports", async () => { const res = await app.request("/v1/status_report", { method: "GET", @@ -15,7 +109,7 @@ test("return all status reports", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); + expect(result.data?.some((r) => r.id === testStatusReportId)).toBe(true); }); test("return all status reports with monitorIds", async () => { @@ -30,16 +124,12 @@ test("return all status reports with monitorIds", async () => { expect(res.status).toBe(200); expect(result.success).toBe(true); - expect(result.data?.length).toBeGreaterThan(0); - // Each status report should have monitorIds defined - for (const statusReport of result.data || []) { - expect(statusReport.monitorIds).toBeDefined(); - expect(Array.isArray(statusReport.monitorIds)).toBe(true); - // Ensure each monitorId is a number - for (const monitorId of statusReport.monitorIds || []) { - expect(typeof monitorId).toBe("number"); - } - } + + const testReport = result.data?.find((r) => r.id === testStatusReportId); + expect(testReport).toBeDefined(); + expect(testReport?.monitorIds).toBeDefined(); + expect(Array.isArray(testReport?.monitorIds)).toBe(true); + expect(testReport?.monitorIds).toContain(testMonitorId); }); test("return empty status reports", async () => { From 1f72762b79371da3b951e0152c4348024be38e99 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:26:08 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- apps/server/src/routes/v1/statusReports/get_all.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/routes/v1/statusReports/get_all.test.ts b/apps/server/src/routes/v1/statusReports/get_all.test.ts index 7cc362dc06..daa698ce38 100644 --- a/apps/server/src/routes/v1/statusReports/get_all.test.ts +++ b/apps/server/src/routes/v1/statusReports/get_all.test.ts @@ -84,7 +84,9 @@ beforeAll(async () => { afterAll(async () => { await db .delete(statusReportsToPageComponents) - .where(eq(statusReportsToPageComponents.statusReportId, testStatusReportId)); + .where( + eq(statusReportsToPageComponents.statusReportId, testStatusReportId), + ); await db .delete(statusReportUpdate) .where(eq(statusReportUpdate.statusReportId, testStatusReportId));