Openstatus
www.openstatus.dev
1import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2import { and, db, eq, inArray } from "@openstatus/db";
3import {
4 maintenance,
5 maintenancesToMonitors,
6 maintenancesToPageComponents,
7 monitor,
8 monitorGroup,
9 monitorsToPages,
10 monitorsToStatusReport,
11 page,
12 pageComponent,
13 pageComponentGroup,
14 statusReport,
15 statusReportUpdate,
16 statusReportsToPageComponents,
17} from "@openstatus/db/src/schema";
18import { flyRegions } from "@openstatus/db/src/schema/constants";
19
20import { appRouter } from "../root";
21import { createInnerTRPCContext } from "../trpc";
22
23/**
24 * Sync Tests: Verify that mutations to legacy tables also sync to new page_component tables
25 *
26 * Table mappings:
27 * - monitor_group -> page_component_groups
28 * - monitors_to_pages -> page_component
29 * - status_report_to_monitors -> status_report_to_page_component
30 * - maintenance_to_monitor -> maintenance_to_page_component
31 */
32
33function getTestContext(limits?: unknown) {
34 return createInnerTRPCContext({
35 req: undefined,
36 session: {
37 user: {
38 id: "1",
39 },
40 },
41 workspace: {
42 id: 1,
43 // @ts-expect-error - test context with partial limits
44 limits: limits || {
45 monitors: 100,
46 periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"],
47 regions: flyRegions,
48 "status-pages": 10,
49 maintenance: true,
50 notifications: 10,
51 "status-subscribers": true,
52 sms: false,
53 pagerduty: true,
54 "password-protection": true,
55 "email-domain-protection": true,
56 "custom-domain": true,
57 },
58 },
59 });
60}
61
62// Test data identifiers
63const TEST_PREFIX = "sync-test";
64let testPageId: number;
65let testMonitorId: number;
66
67const monitorData = {
68 name: `${TEST_PREFIX}-monitor`,
69 url: "https://sync-test.example.com",
70 jobType: "http" as const,
71 method: "GET" as const,
72 periodicity: "1m" as const,
73 regions: [flyRegions[0]],
74 statusAssertions: [],
75 headerAssertions: [],
76 textBodyAssertions: [],
77 notifications: [],
78 pages: [] as number[],
79 tags: [],
80};
81
82beforeAll(async () => {
83 // Clean up any existing test data
84 await db
85 .delete(pageComponent)
86 .where(eq(pageComponent.name, `${TEST_PREFIX}-monitor`));
87 await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-page`));
88 await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
89 await db
90 .delete(monitor)
91 .where(eq(monitor.name, `${TEST_PREFIX}-deletable-monitor`));
92
93 // Create test page first
94 const testPage = await db
95 .insert(page)
96 .values({
97 workspaceId: 1,
98 title: "Sync Test Page",
99 description: "A test page for sync tests",
100 slug: `${TEST_PREFIX}-page`,
101 customDomain: "",
102 })
103 .returning()
104 .get();
105 testPageId = testPage.id;
106
107 // Create test monitor using tRPC
108 const ctx = getTestContext();
109 const caller = appRouter.createCaller(ctx);
110 const createdMonitor = await caller.monitor.create(monitorData);
111 testMonitorId = createdMonitor.id;
112});
113
114afterAll(async () => {
115 // Clean up test data in correct order (dependencies first)
116 await db
117 .delete(maintenancesToPageComponents)
118 .where(
119 inArray(
120 maintenancesToPageComponents.pageComponentId,
121 db
122 .select({ id: pageComponent.id })
123 .from(pageComponent)
124 .where(eq(pageComponent.pageId, testPageId)),
125 ),
126 );
127 await db
128 .delete(statusReportsToPageComponents)
129 .where(
130 inArray(
131 statusReportsToPageComponents.pageComponentId,
132 db
133 .select({ id: pageComponent.id })
134 .from(pageComponent)
135 .where(eq(pageComponent.pageId, testPageId)),
136 ),
137 );
138 await db.delete(pageComponent).where(eq(pageComponent.pageId, testPageId));
139 await db
140 .delete(pageComponentGroup)
141 .where(eq(pageComponentGroup.pageId, testPageId));
142 await db.delete(monitorGroup).where(eq(monitorGroup.pageId, testPageId));
143 await db
144 .delete(monitorsToPages)
145 .where(eq(monitorsToPages.pageId, testPageId));
146 await db
147 .delete(maintenancesToMonitors)
148 .where(eq(maintenancesToMonitors.monitorId, testMonitorId));
149 await db
150 .delete(monitorsToStatusReport)
151 .where(eq(monitorsToStatusReport.monitorId, testMonitorId));
152 await db
153 .delete(statusReportUpdate)
154 .where(
155 inArray(
156 statusReportUpdate.statusReportId,
157 db
158 .select({ id: statusReport.id })
159 .from(statusReport)
160 .where(eq(statusReport.pageId, testPageId)),
161 ),
162 );
163 await db.delete(statusReport).where(eq(statusReport.pageId, testPageId));
164 await db.delete(maintenance).where(eq(maintenance.pageId, testPageId));
165 await db.delete(page).where(eq(page.slug, `${TEST_PREFIX}-page`));
166 await db.delete(monitor).where(eq(monitor.name, `${TEST_PREFIX}-monitor`));
167 await db
168 .delete(monitor)
169 .where(eq(monitor.name, `${TEST_PREFIX}-deletable-monitor`));
170});
171
172describe("Sync: monitors_to_pages -> page_component", () => {
173 test("Creating monitor-to-page relation syncs to page_component", async () => {
174 const ctx = getTestContext();
175 const caller = appRouter.createCaller(ctx);
176
177 // Update monitor to add it to the test page
178 await caller.monitor.update({
179 ...monitorData,
180 id: testMonitorId,
181 pages: [testPageId],
182 });
183
184 // Verify monitors_to_pages was created
185 const monitorToPage = await db.query.monitorsToPages.findFirst({
186 where: and(
187 eq(monitorsToPages.monitorId, testMonitorId),
188 eq(monitorsToPages.pageId, testPageId),
189 ),
190 });
191 expect(monitorToPage).toBeDefined();
192
193 // Verify page_component was synced
194 const component = await db.query.pageComponent.findFirst({
195 where: and(
196 eq(pageComponent.monitorId, testMonitorId),
197 eq(pageComponent.pageId, testPageId),
198 ),
199 });
200 expect(component).toBeDefined();
201 expect(component?.type).toBe("monitor");
202 expect(component?.workspaceId).toBe(1);
203 });
204
205 test("Removing monitor-to-page relation syncs delete to page_component", async () => {
206 const ctx = getTestContext();
207 const caller = appRouter.createCaller(ctx);
208
209 // First add the monitor to page if not already
210 await caller.monitor.update({
211 ...monitorData,
212 id: testMonitorId,
213 pages: [testPageId],
214 });
215
216 // Verify page_component exists
217 let component = await db.query.pageComponent.findFirst({
218 where: and(
219 eq(pageComponent.monitorId, testMonitorId),
220 eq(pageComponent.pageId, testPageId),
221 ),
222 });
223 expect(component).toBeDefined();
224
225 // Remove monitor from page
226 await caller.monitor.update({
227 ...monitorData,
228 id: testMonitorId,
229 pages: [],
230 });
231
232 // Verify page_component was deleted
233 component = await db.query.pageComponent.findFirst({
234 where: and(
235 eq(pageComponent.monitorId, testMonitorId),
236 eq(pageComponent.pageId, testPageId),
237 ),
238 });
239 expect(component).toBeUndefined();
240 });
241});
242
243describe("Sync: page.updateMonitors -> page_component and page_component_groups", () => {
244 test("Adding monitors with groups syncs to page_component and page_component_groups", async () => {
245 const ctx = getTestContext();
246 const caller = appRouter.createCaller(ctx);
247
248 // Update page with monitor in a group
249 await caller.page.updateMonitors({
250 id: testPageId,
251 monitors: [],
252 groups: [
253 {
254 name: `${TEST_PREFIX}-group`,
255 order: 0,
256 monitors: [{ id: testMonitorId, order: 0 }],
257 },
258 ],
259 });
260
261 // Verify monitor_group was created
262 const group = await db.query.monitorGroup.findFirst({
263 where: and(
264 eq(monitorGroup.pageId, testPageId),
265 eq(monitorGroup.name, `${TEST_PREFIX}-group`),
266 ),
267 });
268 expect(group).toBeDefined();
269
270 if (!group) {
271 throw new Error("Group not found");
272 }
273
274 // Verify page_component_groups was synced
275 const componentGroup = await db.query.pageComponentGroup.findFirst({
276 where: eq(pageComponentGroup.id, group.id),
277 });
278 expect(componentGroup).toBeDefined();
279 expect(componentGroup?.name).toBe(`${TEST_PREFIX}-group`);
280 expect(componentGroup?.pageId).toBe(testPageId);
281
282 // Verify page_component was synced with group reference
283 const component = await db.query.pageComponent.findFirst({
284 where: and(
285 eq(pageComponent.monitorId, testMonitorId),
286 eq(pageComponent.pageId, testPageId),
287 ),
288 });
289 expect(component).toBeDefined();
290 expect(component?.groupId).toBe(group.id);
291 });
292
293 test("Updating page monitors syncs changes to page_component", async () => {
294 const ctx = getTestContext();
295 const caller = appRouter.createCaller(ctx);
296
297 // First, set up with a group
298 await caller.page.updateMonitors({
299 id: testPageId,
300 monitors: [],
301 groups: [
302 {
303 name: `${TEST_PREFIX}-group`,
304 order: 0,
305 monitors: [{ id: testMonitorId, order: 0 }],
306 },
307 ],
308 });
309
310 // Now update to remove the group and add monitor directly
311 await caller.page.updateMonitors({
312 id: testPageId,
313 monitors: [{ id: testMonitorId, order: 0 }],
314 groups: [],
315 });
316
317 // Verify monitor_group was deleted
318 const group = await db.query.monitorGroup.findFirst({
319 where: and(
320 eq(monitorGroup.pageId, testPageId),
321 eq(monitorGroup.name, `${TEST_PREFIX}-group`),
322 ),
323 });
324 expect(group).toBeUndefined();
325
326 // Verify page_component still exists but without group
327 const component = await db.query.pageComponent.findFirst({
328 where: and(
329 eq(pageComponent.monitorId, testMonitorId),
330 eq(pageComponent.pageId, testPageId),
331 ),
332 });
333 expect(component).toBeDefined();
334 expect(component?.groupId).toBeNull();
335 });
336});
337
338describe("Sync: maintenance_to_monitor -> maintenance_to_page_component", () => {
339 let testMaintenanceId: number;
340
341 beforeAll(async () => {
342 const ctx = getTestContext();
343 const caller = appRouter.createCaller(ctx);
344
345 // Ensure monitor is on the page first
346 await caller.monitor.update({
347 ...monitorData,
348 id: testMonitorId,
349 pages: [testPageId],
350 });
351 });
352
353 afterAll(async () => {
354 if (testMaintenanceId) {
355 await db
356 .delete(maintenancesToPageComponents)
357 .where(
358 eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId),
359 );
360 await db
361 .delete(maintenancesToMonitors)
362 .where(eq(maintenancesToMonitors.maintenanceId, testMaintenanceId));
363 await db.delete(maintenance).where(eq(maintenance.id, testMaintenanceId));
364 }
365 });
366
367 test("Creating maintenance with monitors syncs to maintenance_to_page_component", async () => {
368 const ctx = getTestContext();
369 const caller = appRouter.createCaller(ctx);
370
371 const from = new Date();
372 const to = new Date(from.getTime() + 1000 * 60 * 60);
373
374 const createdMaintenance = await caller.maintenance.new({
375 title: `${TEST_PREFIX} Maintenance`,
376 message: "Test maintenance for sync",
377 startDate: from,
378 endDate: to,
379 pageId: testPageId,
380 monitors: [testMonitorId],
381 });
382 testMaintenanceId = createdMaintenance.id;
383
384 // Verify maintenance_to_monitor was created
385 const maintenanceToMonitor =
386 await db.query.maintenancesToMonitors.findFirst({
387 where: and(
388 eq(maintenancesToMonitors.maintenanceId, testMaintenanceId),
389 eq(maintenancesToMonitors.monitorId, testMonitorId),
390 ),
391 });
392 expect(maintenanceToMonitor).toBeDefined();
393
394 // Verify maintenance_to_page_component was synced
395 const component = await db.query.pageComponent.findFirst({
396 where: and(
397 eq(pageComponent.monitorId, testMonitorId),
398 eq(pageComponent.pageId, testPageId),
399 ),
400 });
401
402 if (component) {
403 const maintenanceToComponent =
404 await db.query.maintenancesToPageComponents.findFirst({
405 where: and(
406 eq(maintenancesToPageComponents.maintenanceId, testMaintenanceId),
407 eq(maintenancesToPageComponents.pageComponentId, component.id),
408 ),
409 });
410 expect(maintenanceToComponent).toBeDefined();
411 }
412 });
413
414 test("Updating maintenance monitors syncs to maintenance_to_page_component", async () => {
415 const ctx = getTestContext();
416 const caller = appRouter.createCaller(ctx);
417
418 // Skip if no maintenance was created
419 if (!testMaintenanceId) return;
420
421 const from = new Date();
422 const to = new Date(from.getTime() + 1000 * 60 * 60);
423
424 // Update maintenance to remove monitors
425 await caller.maintenance.update({
426 id: testMaintenanceId,
427 title: `${TEST_PREFIX} Maintenance`,
428 message: "Updated maintenance",
429 startDate: from,
430 endDate: to,
431 monitors: [],
432 });
433
434 // Verify maintenance_to_monitor was deleted
435 const maintenanceToMonitor =
436 await db.query.maintenancesToMonitors.findFirst({
437 where: and(
438 eq(maintenancesToMonitors.maintenanceId, testMaintenanceId),
439 eq(maintenancesToMonitors.monitorId, testMonitorId),
440 ),
441 });
442 expect(maintenanceToMonitor).toBeUndefined();
443
444 // Verify maintenance_to_page_component was also deleted
445 const maintenanceToComponent =
446 await db.query.maintenancesToPageComponents.findFirst({
447 where: eq(
448 maintenancesToPageComponents.maintenanceId,
449 testMaintenanceId,
450 ),
451 });
452 expect(maintenanceToComponent).toBeUndefined();
453 });
454});
455
456describe("Sync: status_report_to_monitors -> status_report_to_page_component", () => {
457 let testStatusReportId: number;
458
459 beforeAll(async () => {
460 const ctx = getTestContext();
461 const caller = appRouter.createCaller(ctx);
462
463 // Ensure monitor is on the page first
464 await caller.monitor.update({
465 ...monitorData,
466 id: testMonitorId,
467 pages: [testPageId],
468 });
469 });
470
471 afterAll(async () => {
472 if (testStatusReportId) {
473 await db
474 .delete(statusReportsToPageComponents)
475 .where(
476 eq(statusReportsToPageComponents.statusReportId, testStatusReportId),
477 );
478 await db
479 .delete(monitorsToStatusReport)
480 .where(eq(monitorsToStatusReport.statusReportId, testStatusReportId));
481 await db
482 .delete(statusReportUpdate)
483 .where(eq(statusReportUpdate.statusReportId, testStatusReportId));
484 await db
485 .delete(statusReport)
486 .where(eq(statusReport.id, testStatusReportId));
487 }
488 });
489
490 test("Creating status report with monitors syncs to status_report_to_page_component", async () => {
491 const ctx = getTestContext();
492 const caller = appRouter.createCaller(ctx);
493
494 const createdReport = await caller.statusReport.create({
495 title: `${TEST_PREFIX} Status Report`,
496 status: "investigating",
497 message: "Test status report for sync",
498 pageId: testPageId,
499 monitors: [testMonitorId],
500 date: new Date(),
501 });
502 testStatusReportId = createdReport.statusReportId;
503
504 // Verify status_report_to_monitors was created
505 const reportToMonitor = await db.query.monitorsToStatusReport.findFirst({
506 where: and(
507 eq(monitorsToStatusReport.statusReportId, testStatusReportId),
508 eq(monitorsToStatusReport.monitorId, testMonitorId),
509 ),
510 });
511 expect(reportToMonitor).toBeDefined();
512
513 // Verify status_report_to_page_component was synced
514 const component = await db.query.pageComponent.findFirst({
515 where: and(
516 eq(pageComponent.monitorId, testMonitorId),
517 eq(pageComponent.pageId, testPageId),
518 ),
519 });
520
521 if (component) {
522 const reportToComponent =
523 await db.query.statusReportsToPageComponents.findFirst({
524 where: and(
525 eq(
526 statusReportsToPageComponents.statusReportId,
527 testStatusReportId,
528 ),
529 eq(statusReportsToPageComponents.pageComponentId, component.id),
530 ),
531 });
532 expect(reportToComponent).toBeDefined();
533 }
534 });
535
536 test("Updating status report monitors syncs to status_report_to_page_component", async () => {
537 const ctx = getTestContext();
538 const caller = appRouter.createCaller(ctx);
539
540 // Skip if no status report was created
541 if (!testStatusReportId) return;
542
543 // Update status to remove monitors (using updateStatus procedure)
544 await caller.statusReport.updateStatus({
545 id: testStatusReportId,
546 status: "resolved",
547 monitors: [],
548 title: `${TEST_PREFIX} Status Report`,
549 });
550
551 // Verify status_report_to_monitors was deleted
552 const reportToMonitor = await db.query.monitorsToStatusReport.findFirst({
553 where: and(
554 eq(monitorsToStatusReport.statusReportId, testStatusReportId),
555 eq(monitorsToStatusReport.monitorId, testMonitorId),
556 ),
557 });
558 expect(reportToMonitor).toBeUndefined();
559
560 // Verify status_report_to_page_component was also deleted
561 const reportToComponent =
562 await db.query.statusReportsToPageComponents.findFirst({
563 where: eq(
564 statusReportsToPageComponents.statusReportId,
565 testStatusReportId,
566 ),
567 });
568 expect(reportToComponent).toBeUndefined();
569 });
570});
571
572describe("Sync: monitor deletion cascades to page_component tables", () => {
573 let deletableMonitorId: number;
574
575 beforeAll(async () => {
576 const ctx = getTestContext();
577 const caller = appRouter.createCaller(ctx);
578
579 // Create a monitor specifically for deletion tests
580 const deletableMonitor = await caller.monitor.create({
581 ...monitorData,
582 name: `${TEST_PREFIX}-deletable-monitor`,
583 url: "https://delete-test.example.com",
584 pages: [testPageId],
585 });
586 deletableMonitorId = deletableMonitor.id;
587 });
588
589 test("Deleting monitor removes related page_component entries", async () => {
590 const ctx = getTestContext();
591 const caller = appRouter.createCaller(ctx);
592
593 // Verify page_component exists before deletion
594 let component = await db.query.pageComponent.findFirst({
595 where: eq(pageComponent.monitorId, deletableMonitorId),
596 });
597 expect(component).toBeDefined();
598
599 // Delete the monitor
600 await caller.monitor.delete({ id: deletableMonitorId });
601
602 // Verify page_component was removed
603 component = await db.query.pageComponent.findFirst({
604 where: eq(pageComponent.monitorId, deletableMonitorId),
605 });
606 expect(component).toBeUndefined();
607 });
608});