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