Skip to content

Commit 3a0ab1f

Browse files
fix: resolve flaky tests caused by shared state between parallel test files (#1957)
* 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 * 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. * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9fdca3c commit 3a0ab1f

File tree

6 files changed

+365
-47
lines changed

6 files changed

+365
-47
lines changed

apps/server/src/routes/v1/incidents/get_all.test.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,58 @@
1-
import { expect, test } from "bun:test";
1+
import { afterAll, beforeAll, expect, test } from "bun:test";
2+
import { db, eq } from "@openstatus/db";
3+
import { incidentTable, monitor } from "@openstatus/db/src/schema";
24

35
import { app } from "@/index";
46
import { IncidentSchema } from "./schema";
57

8+
const TEST_PREFIX = "v1-incident-getall-test";
9+
let testMonitorId: number;
10+
let testIncidentId: number;
11+
12+
beforeAll(async () => {
13+
await db
14+
.delete(incidentTable)
15+
.where(eq(incidentTable.title, `${TEST_PREFIX}-incident`));
16+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
17+
18+
const mon = await db
19+
.insert(monitor)
20+
.values({
21+
workspaceId: 1,
22+
name: `${TEST_PREFIX}-monitor`,
23+
url: "https://test.example.com",
24+
periodicity: "1m",
25+
active: true,
26+
regions: "ams",
27+
jobType: "http",
28+
method: "GET",
29+
timeout: 30000,
30+
})
31+
.returning()
32+
.get();
33+
testMonitorId = mon.id;
34+
35+
const incident = await db
36+
.insert(incidentTable)
37+
.values({
38+
workspaceId: 1,
39+
monitorId: testMonitorId,
40+
title: `${TEST_PREFIX}-incident`,
41+
status: "investigating",
42+
startedAt: new Date("2099-01-01T00:00:00Z"),
43+
})
44+
.returning()
45+
.get();
46+
testIncidentId = incident.id;
47+
});
48+
49+
afterAll(async () => {
50+
await db
51+
.delete(incidentTable)
52+
.where(eq(incidentTable.title, `${TEST_PREFIX}-incident`));
53+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
54+
});
55+
656
test("return all incidents", async () => {
757
const res = await app.request("/v1/incident", {
858
method: "GET",
@@ -15,14 +65,14 @@ test("return all incidents", async () => {
1565

1666
expect(res.status).toBe(200);
1767
expect(result.success).toBe(true);
18-
expect(result.data?.length).toBeGreaterThan(0);
68+
expect(result.data?.some((i) => i.id === testIncidentId)).toBe(true);
1969
});
2070

2171
test("return empty incidents", async () => {
2272
const res = await app.request("/v1/incident", {
2373
method: "GET",
2474
headers: {
25-
"x-openstatus-key": "2",
75+
"x-openstatus-key": "3",
2676
},
2777
});
2878

apps/server/src/routes/v1/maintenances/get_all.test.ts

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,93 @@
1-
import { expect, test } from "bun:test";
1+
import { afterAll, beforeAll, expect, test } from "bun:test";
2+
import { db, eq } from "@openstatus/db";
3+
import {
4+
maintenance,
5+
maintenancesToPageComponents,
6+
monitor,
7+
pageComponent,
8+
} from "@openstatus/db/src/schema";
9+
210
import { app } from "@/index";
311
import { MaintenanceSchema } from "./schema";
412

13+
const TEST_PREFIX = "v1-maint-getall-test";
14+
let testMonitorId: number;
15+
let testPageComponentId: number;
16+
let testMaintenanceId: number;
17+
18+
beforeAll(async () => {
19+
await db
20+
.delete(maintenance)
21+
.where(eq(maintenance.title, `${TEST_PREFIX}-maint`));
22+
await db
23+
.delete(pageComponent)
24+
.where(eq(pageComponent.name, `${TEST_PREFIX}-component`));
25+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
26+
27+
const mon = await db
28+
.insert(monitor)
29+
.values({
30+
workspaceId: 1,
31+
name: `${TEST_PREFIX}-monitor`,
32+
url: "https://test.example.com",
33+
periodicity: "1m",
34+
active: true,
35+
regions: "ams",
36+
jobType: "http",
37+
method: "GET",
38+
timeout: 30000,
39+
})
40+
.returning()
41+
.get();
42+
testMonitorId = mon.id;
43+
44+
const comp = await db
45+
.insert(pageComponent)
46+
.values({
47+
workspaceId: 1,
48+
pageId: 1,
49+
monitorId: testMonitorId,
50+
type: "monitor",
51+
name: `${TEST_PREFIX}-component`,
52+
order: 200,
53+
})
54+
.returning()
55+
.get();
56+
testPageComponentId = comp.id;
57+
58+
const maint = await db
59+
.insert(maintenance)
60+
.values({
61+
workspaceId: 1,
62+
pageId: 1,
63+
title: `${TEST_PREFIX}-maint`,
64+
message: "Test maintenance",
65+
from: new Date("2099-01-01T00:00:00Z"),
66+
to: new Date("2099-01-02T00:00:00Z"),
67+
})
68+
.returning()
69+
.get();
70+
testMaintenanceId = maint.id;
71+
72+
await db.insert(maintenancesToPageComponents).values({
73+
maintenanceId: testMaintenanceId,
74+
pageComponentId: testPageComponentId,
75+
});
76+
});
77+
78+
afterAll(async () => {
79+
await db
80+
.delete(maintenancesToPageComponents)
81+
.where(eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId));
82+
await db
83+
.delete(maintenance)
84+
.where(eq(maintenance.title, `${TEST_PREFIX}-maint`));
85+
await db
86+
.delete(pageComponent)
87+
.where(eq(pageComponent.name, `${TEST_PREFIX}-component`));
88+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
89+
});
90+
591
test("return all maintenances", async () => {
692
const res = await app.request("/v1/maintenance", {
793
method: "GET",
@@ -11,10 +97,10 @@ test("return all maintenances", async () => {
1197
});
1298

1399
const result = MaintenanceSchema.array().safeParse(await res.json());
14-
console.log(result);
100+
15101
expect(res.status).toBe(200);
16102
expect(result.success).toBe(true);
17-
expect(result.data?.length).toBeGreaterThan(0);
103+
expect(result.data?.some((m) => m.id === testMaintenanceId)).toBe(true);
18104
});
19105

20106
test("return all maintenances with monitorIds", async () => {
@@ -29,19 +115,19 @@ test("return all maintenances with monitorIds", async () => {
29115

30116
expect(res.status).toBe(200);
31117
expect(result.success).toBe(true);
32-
expect(result.data?.length).toBeGreaterThan(0);
33-
// Each maintenance should have monitorIds defined
34-
for (const maintenance of result.data || []) {
35-
expect(maintenance.monitorIds).toBeDefined();
36-
expect(Array.isArray(maintenance.monitorIds)).toBe(true);
37-
}
118+
119+
const testMaint = result.data?.find((m) => m.id === testMaintenanceId);
120+
expect(testMaint).toBeDefined();
121+
expect(testMaint?.monitorIds).toBeDefined();
122+
expect(Array.isArray(testMaint?.monitorIds)).toBe(true);
123+
expect(testMaint?.monitorIds).toContain(testMonitorId);
38124
});
39125

40126
test("return empty maintenances", async () => {
41127
const res = await app.request("/v1/maintenance", {
42128
method: "GET",
43129
headers: {
44-
"x-openstatus-key": "2",
130+
"x-openstatus-key": "3",
45131
},
46132
});
47133

apps/server/src/routes/v1/notifications/get_all.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
1-
import { expect, test } from "bun:test";
1+
import { afterAll, beforeAll, expect, test } from "bun:test";
2+
import { db, eq } from "@openstatus/db";
3+
import { notification } from "@openstatus/db/src/schema";
24

35
import { app } from "@/index";
46
import { NotificationSchema } from "./schema";
57

8+
const TEST_PREFIX = "v1-notif-getall-test";
9+
let testNotificationId: number;
10+
11+
beforeAll(async () => {
12+
await db
13+
.delete(notification)
14+
.where(eq(notification.name, `${TEST_PREFIX}-email`));
15+
16+
const notif = await db
17+
.insert(notification)
18+
.values({
19+
workspaceId: 1,
20+
name: `${TEST_PREFIX}-email`,
21+
provider: "email",
22+
data: '{"email":"test@test.com"}',
23+
})
24+
.returning()
25+
.get();
26+
testNotificationId = notif.id;
27+
});
28+
29+
afterAll(async () => {
30+
await db
31+
.delete(notification)
32+
.where(eq(notification.name, `${TEST_PREFIX}-email`));
33+
});
34+
635
test("return all notifications", async () => {
736
const res = await app.request("/v1/notification", {
837
method: "GET",
@@ -15,14 +44,14 @@ test("return all notifications", async () => {
1544

1645
expect(res.status).toBe(200);
1746
expect(result.success).toBe(true);
18-
expect(result.data?.length).toBeGreaterThan(0);
47+
expect(result.data?.some((n) => n.id === testNotificationId)).toBe(true);
1948
});
2049

2150
test("return empty notifications", async () => {
2251
const res = await app.request("/v1/notification", {
2352
method: "GET",
2453
headers: {
25-
"x-openstatus-key": "2",
54+
"x-openstatus-key": "3",
2655
},
2756
});
2857

apps/server/src/routes/v1/pages/get_all.test.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,69 @@
1-
import { expect, test } from "bun:test";
1+
import { afterAll, beforeAll, expect, test } from "bun:test";
2+
import { db, eq } from "@openstatus/db";
3+
import { monitor, page, pageComponent } from "@openstatus/db/src/schema";
24

35
import { app } from "@/index";
46
import { PageSchema } from "./schema";
57

8+
const TEST_PREFIX = "v1-page-getall-test";
9+
let testMonitorId: number;
10+
let testPageId: number;
11+
12+
beforeAll(async () => {
13+
await db
14+
.delete(pageComponent)
15+
.where(eq(pageComponent.name, `${TEST_PREFIX}-component`));
16+
await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));
17+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
18+
19+
const mon = await db
20+
.insert(monitor)
21+
.values({
22+
workspaceId: 1,
23+
name: `${TEST_PREFIX}-monitor`,
24+
url: "https://test.example.com",
25+
periodicity: "1m",
26+
active: true,
27+
regions: "ams",
28+
jobType: "http",
29+
method: "GET",
30+
timeout: 30000,
31+
})
32+
.returning()
33+
.get();
34+
testMonitorId = mon.id;
35+
36+
const p = await db
37+
.insert(page)
38+
.values({
39+
workspaceId: 1,
40+
title: `${TEST_PREFIX}-page`,
41+
slug: `${TEST_PREFIX}-slug`,
42+
description: "Test page",
43+
customDomain: "",
44+
})
45+
.returning()
46+
.get();
47+
testPageId = p.id;
48+
49+
await db.insert(pageComponent).values({
50+
workspaceId: 1,
51+
pageId: testPageId,
52+
monitorId: testMonitorId,
53+
type: "monitor",
54+
name: `${TEST_PREFIX}-component`,
55+
order: 0,
56+
});
57+
});
58+
59+
afterAll(async () => {
60+
await db
61+
.delete(pageComponent)
62+
.where(eq(pageComponent.name, `${TEST_PREFIX}-component`));
63+
await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-slug`));
64+
await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
65+
});
66+
667
test("return all pages", async () => {
768
const res = await app.request("/v1/page", {
869
method: "GET",
@@ -15,14 +76,14 @@ test("return all pages", async () => {
1576

1677
expect(res.status).toBe(200);
1778
expect(result.success).toBe(true);
18-
expect(result.data?.length).toBeGreaterThan(0);
79+
expect(result.data?.some((p) => p.id === testPageId)).toBe(true);
1980
});
2081

2182
test("return empty pages", async () => {
2283
const res = await app.request("/v1/page", {
2384
method: "GET",
2485
headers: {
25-
"x-openstatus-key": "2",
86+
"x-openstatus-key": "3",
2687
},
2788
});
2889

apps/server/src/routes/v1/pages/post.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,6 @@ export function registerPostPage(api: typeof pagesApi) {
6565
});
6666
}
6767

68-
const count = (
69-
await db
70-
.select({ count: sql<number>`count(*)` })
71-
.from(page)
72-
.where(eq(page.workspaceId, workspaceId))
73-
.all()
74-
)[0].count;
75-
76-
if (count >= limits["status-pages"]) {
77-
throw new OpenStatusApiError({
78-
code: "PAYMENT_REQUIRED",
79-
message: "Upgrade for more status pages",
80-
});
81-
}
82-
8368
if (
8469
!limits["password-protection"] &&
8570
(input?.passwordProtected || input?.password)
@@ -110,6 +95,21 @@ export function registerPostPage(api: typeof pagesApi) {
11095
});
11196
}
11297

98+
const count = (
99+
await db
100+
.select({ count: sql<number>`count(*)` })
101+
.from(page)
102+
.where(eq(page.workspaceId, workspaceId))
103+
.all()
104+
)[0].count;
105+
106+
if (count >= limits["status-pages"]) {
107+
throw new OpenStatusApiError({
108+
code: "PAYMENT_REQUIRED",
109+
message: "Upgrade for more status pages",
110+
});
111+
}
112+
113113
if (subdomainSafeList.includes(input.slug)) {
114114
throw new OpenStatusApiError({
115115
code: "BAD_REQUEST",

0 commit comments

Comments
 (0)