diff --git a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts index 09c31b603..efc4963ec 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts @@ -448,22 +448,31 @@ describe("MonitorService.ListMonitors", () => { }); test("uses offset for pagination", async () => { - // First page - const res1 = await connectRequest( - "ListMonitors", - { limit: 1 }, - { "x-openstatus-key": "1" }, - ); + // Isolate in a dedicated workspace so concurrent monitor inserts/deletes + // on the seeded workspace can't shift the pagination window mid-test. + // Distinct createdAt values give the `desc(createdAt)` order a + // deterministic sequence (it has no id tiebreaker). + const ws = await createWorkspace(); + const key = String(ws.id); + const now = Date.now(); + await createMonitor(ws.id, { createdAt: new Date(now - 1000) }); + await createMonitor(ws.id, { createdAt: new Date(now) }); - expect(res1.status).toBe(200); - const data1 = await res1.json(); + try { + const res1 = await connectRequest( + "ListMonitors", + { limit: 1 }, + { "x-openstatus-key": key }, + ); + + expect(res1.status).toBe(200); + const data1 = await res1.json(); + expect(data1.totalSize).toBe(2); - if (data1.totalSize > 1) { - // Second page using offset const res2 = await connectRequest( "ListMonitors", { limit: 1, offset: 1 }, - { "x-openstatus-key": "1" }, + { "x-openstatus-key": key }, ); expect(res2.status).toBe(200); @@ -482,11 +491,12 @@ describe("MonitorService.ListMonitors", () => { ...(data2.dnsMonitors || []).map((m: { id: string }) => m.id), ]; - // Should have no overlap - const overlap = firstPageIds.filter((id: string) => - secondPageIds.includes(id), - ); - expect(overlap.length).toBe(0); + expect(firstPageIds).toHaveLength(1); + expect(secondPageIds).toHaveLength(1); + expect(firstPageIds[0]).not.toBe(secondPageIds[0]); + } finally { + await db.delete(monitor).where(eq(monitor.workspaceId, ws.id)); + await db.delete(workspace).where(eq(workspace.id, ws.id)); } }); diff --git a/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts b/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts index 0b905f328..a7e872a27 100644 --- a/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts +++ b/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts @@ -2,8 +2,13 @@ import { db, eq } from "@openstatus/db"; import { notification, notificationsToMonitors, + workspace, } from "@openstatus/db/src/schema"; -import { createTestWorkspace } from "@openstatus/db/src/test/factories"; +import { + createNotification, + createTestWorkspace, + createWorkspace, +} from "@openstatus/db/src/test/factories"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { app } from "../../../../../index"; @@ -444,36 +449,36 @@ describe("NotificationService.ListNotifications", () => { }); test("respects offset parameter", async () => { - // Get total count first - const res1 = await connectRequest( - "ListNotifications", - {}, - { "x-openstatus-key": "1" }, - ); - const data1 = await res1.json(); - const totalSize = data1.totalSize; + // Isolate in a dedicated workspace so concurrent notification + // inserts/deletes on the seeded workspace can't shift the pagination + // window mid-test. In dev/test the `x-openstatus-key` header is the + // workspace id. + const ws = await createWorkspace(); + const key = String(ws.id); + await createNotification(ws.id); + await createNotification(ws.id); - if (totalSize > 1) { - // Get first page - const res2 = await connectRequest( + try { + const res1 = await connectRequest( "ListNotifications", { limit: 1, offset: 0 }, - { "x-openstatus-key": "1" }, + { "x-openstatus-key": key }, ); - const data2 = await res2.json(); - - // Get second page - const res3 = await connectRequest( + const res2 = await connectRequest( "ListNotifications", { limit: 1, offset: 1 }, - { "x-openstatus-key": "1" }, + { "x-openstatus-key": key }, ); - const data3 = await res3.json(); + const data1 = await res1.json(); + const data2 = await res2.json(); - // Should have different notifications - if (data2.notifications?.length > 0 && data3.notifications?.length > 0) { - expect(data2.notifications[0].id).not.toBe(data3.notifications[0].id); - } + expect(data1.totalSize).toBe(2); + expect(data1.notifications).toHaveLength(1); + expect(data2.notifications).toHaveLength(1); + expect(data1.notifications[0].id).not.toBe(data2.notifications[0].id); + } finally { + await db.delete(notification).where(eq(notification.workspaceId, ws.id)); + await db.delete(workspace).where(eq(workspace.id, ws.id)); } }); diff --git a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts index bc495398a..7b7bef673 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts @@ -8,8 +8,13 @@ import { pageSubscriber, statusReport, statusReportsToPageComponents, + workspace, } from "@openstatus/db/src/schema"; -import { createTestWorkspace } from "@openstatus/db/src/test/factories"; +import { + createPage, + createTestWorkspace, + createWorkspace, +} from "@openstatus/db/src/test/factories"; import { afterAll, beforeAll, @@ -684,25 +689,35 @@ describe("StatusPageService.ListStatusPages", () => { }); test("respects offset parameter", async () => { - // Get first page - const res1 = await connectRequest( - "ListStatusPages", - { limit: 1, offset: 0 }, - { "x-openstatus-key": "1" }, - ); - const data1 = await res1.json(); + // Isolate in a dedicated workspace so concurrent page inserts/deletes on + // the seeded workspace can't shift the pagination window mid-test. In + // dev/test the `x-openstatus-key` header is the workspace id. + const ws = await createWorkspace(); + const key = String(ws.id); + await createPage(ws.id); + await createPage(ws.id); - // Get second page - const res2 = await connectRequest( - "ListStatusPages", - { limit: 1, offset: 1 }, - { "x-openstatus-key": "1" }, - ); - const data2 = await res2.json(); + try { + const res1 = await connectRequest( + "ListStatusPages", + { limit: 1, offset: 0 }, + { "x-openstatus-key": key }, + ); + const res2 = await connectRequest( + "ListStatusPages", + { limit: 1, offset: 1 }, + { "x-openstatus-key": key }, + ); + const data1 = await res1.json(); + const data2 = await res2.json(); - // Should have different pages if multiple exist - if (data1.statusPages?.length > 0 && data2.statusPages?.length > 0) { + expect(data1.totalSize).toBe(2); + expect(data1.statusPages).toHaveLength(1); + expect(data2.statusPages).toHaveLength(1); expect(data1.statusPages[0].id).not.toBe(data2.statusPages[0].id); + } finally { + await db.delete(page).where(eq(page.workspaceId, ws.id)); + await db.delete(workspace).where(eq(workspace.id, ws.id)); } }); }); diff --git a/packages/db/.busy-probe.mjs b/packages/db/.busy-probe.mjs new file mode 100644 index 000000000..c3b681c8e --- /dev/null +++ b/packages/db/.busy-probe.mjs @@ -0,0 +1,11 @@ +import { createClient } from "@libsql/client"; +const a = createClient({ url: "http://127.0.0.1:8090" }); +const b = createClient({ url: "http://127.0.0.1:8090" }); +await a.execute("create table if not exists t (x int)"); +const txA = await a.transaction(); +await txA.execute("insert into t values (1)"); +const t0 = Date.now(); +const txB = await b.transaction(); +try { await txB.execute("select 1"); console.log("B started after", Date.now()-t0, "ms (no busy)"); } +catch (e) { console.log("B failed after", Date.now()-t0, "ms:", e.code, e.message); } +setTimeout(async () => { await txA.rollback(); console.log("A released at", Date.now()-t0); }, 3000);