Openstatus www.openstatus.dev
at main 506 lines 15 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"; 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});