Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 608 lines 18 kB view raw
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});