Openstatus www.openstatus.dev
at main 1165 lines 31 kB view raw
1import { describe, expect, it } from "bun:test"; 2import type { 3 Incident, 4 Maintenance, 5 PageComponent, 6 StatusReport, 7 StatusReportUpdate, 8} from "@openstatus/db/src/schema"; 9import { 10 fillStatusDataFor45Days, 11 getEvents, 12 getUptime, 13 setDataByType, 14} from "./statusPage.utils"; 15 16type StatusData = { 17 day: string; 18 count: number; 19 ok: number; 20 degraded: number; 21 error: number; 22 monitorId: string; 23}; 24 25type Event = { 26 id: number; 27 name: string; 28 from: Date; 29 to: Date | null; 30 type: "maintenance" | "incident" | "report"; 31 status: "success" | "degraded" | "error" | "info"; 32}; 33 34// Helper functions to create test data 35function createStatusData( 36 daysAgo: number, 37 ok = 0, 38 degraded = 0, 39 error = 0, 40): StatusData { 41 const date = new Date(); 42 date.setDate(date.getDate() - daysAgo); 43 date.setUTCHours(0, 0, 0, 0); 44 45 return { 46 day: date.toISOString(), 47 count: ok + degraded + error, 48 ok, 49 degraded, 50 error, 51 monitorId: "1", 52 }; 53} 54 55function createIncident(id: number, daysAgo: number, durationHours = 1): Event { 56 const from = new Date(); 57 from.setDate(from.getDate() - daysAgo); 58 from.setHours(from.getHours() - durationHours); 59 60 const to = new Date(from); 61 to.setHours(to.getHours() + durationHours); 62 63 return { 64 id, 65 name: "Downtime", 66 from, 67 to, 68 type: "incident", 69 status: "error", 70 }; 71} 72 73function createReport(id: number, daysAgo: number, durationHours = 2): Event { 74 const from = new Date(); 75 from.setDate(from.getDate() - daysAgo); 76 from.setHours(from.getHours() - durationHours); 77 78 const to = new Date(from); 79 to.setHours(to.getHours() + durationHours); 80 81 return { 82 id, 83 name: "Performance Issues", 84 from, 85 to, 86 type: "report", 87 status: "degraded", 88 }; 89} 90 91function createMaintenance( 92 id: number, 93 daysAgo: number, 94 durationHours = 1, 95): Event { 96 const from = new Date(); 97 from.setDate(from.getDate() - daysAgo); 98 from.setHours(from.getHours() - durationHours); 99 100 const to = new Date(from); 101 to.setHours(to.getHours() + durationHours); 102 103 return { 104 id, 105 name: "Scheduled Maintenance", 106 from, 107 to, 108 type: "maintenance", 109 status: "info", 110 }; 111} 112 113describe("setDataByType", () => { 114 describe("barType: absolute", () => { 115 it("should show proportional bar segments with error-only incident", () => { 116 const data = [createStatusData(0, 100, 0, 0)]; 117 const events = [createIncident(1, 0, 2)]; 118 119 const result = setDataByType({ 120 events, 121 data, 122 cardType: "requests", 123 barType: "absolute", 124 }); 125 126 expect(result).toHaveLength(1); 127 expect(result[0].bar).toHaveLength(2); 128 expect(result[0].bar[0].status).toBe("success"); 129 expect(result[0].bar[1].status).toBe("error"); 130 // Should have uptime and downtime segments 131 expect(result[0].bar[0].height).toBeGreaterThan(0); 132 expect(result[0].bar[1].height).toBeGreaterThan(0); 133 }); 134 135 it("should show proportional segments with multiple event types", () => { 136 const data = [createStatusData(0, 100, 0, 0)]; 137 const events = [ 138 createIncident(1, 0, 1), 139 createReport(2, 0, 2), 140 createMaintenance(3, 0, 1), 141 ]; 142 143 const result = setDataByType({ 144 events, 145 data, 146 cardType: "requests", 147 barType: "absolute", 148 }); 149 150 expect(result[0].bar.length).toBeGreaterThan(1); 151 // Should include info, degraded, and error segments 152 const statuses = result[0].bar.map((b) => b.status); 153 expect(statuses).toContain("error"); 154 expect(statuses).toContain("degraded"); 155 expect(statuses).toContain("info"); 156 }); 157 158 it("should show empty bar when no data available", () => { 159 const data = [createStatusData(0, 0, 0, 0)]; 160 const events: Event[] = []; 161 162 const result = setDataByType({ 163 events, 164 data, 165 cardType: "requests", 166 barType: "absolute", 167 }); 168 169 expect(result[0].bar).toHaveLength(1); 170 expect(result[0].bar[0].status).toBe("empty"); 171 expect(result[0].bar[0].height).toBe(100); 172 }); 173 174 it("should show operational bar with duration cardType and no events", () => { 175 const data = [createStatusData(0, 100, 0, 0)]; 176 const events: Event[] = []; 177 178 const result = setDataByType({ 179 events, 180 data, 181 cardType: "duration", 182 barType: "absolute", 183 }); 184 185 expect(result[0].bar).toHaveLength(1); 186 expect(result[0].bar[0].status).toBe("success"); 187 expect(result[0].bar[0].height).toBe(100); 188 }); 189 190 it("should show proportional status segments with mixed data and no events", () => { 191 const data = [createStatusData(0, 80, 15, 5)]; 192 const events: Event[] = []; 193 194 const result = setDataByType({ 195 events, 196 data, 197 cardType: "requests", 198 barType: "absolute", 199 }); 200 201 expect(result[0].bar.length).toBeGreaterThan(1); 202 const statuses = result[0].bar.map((b) => b.status); 203 expect(statuses).toContain("success"); 204 expect(statuses).toContain("degraded"); 205 expect(statuses).toContain("error"); 206 }); 207 }); 208 209 describe("barType: dominant", () => { 210 it("should show error as dominant status when incident exists", () => { 211 const data = [createStatusData(0, 100, 0, 0)]; 212 const events = [createIncident(1, 0)]; 213 214 const result = setDataByType({ 215 events, 216 data, 217 cardType: "requests", 218 barType: "dominant", 219 }); 220 221 expect(result[0].bar).toHaveLength(1); 222 expect(result[0].bar[0].status).toBe("error"); 223 expect(result[0].bar[0].height).toBe(100); 224 }); 225 226 it("should show degraded when only reports exist", () => { 227 const data = [createStatusData(0, 100, 0, 0)]; 228 const events = [createReport(1, 0)]; 229 230 const result = setDataByType({ 231 events, 232 data, 233 cardType: "requests", 234 barType: "dominant", 235 }); 236 237 expect(result[0].bar).toHaveLength(1); 238 expect(result[0].bar[0].status).toBe("degraded"); 239 expect(result[0].bar[0].height).toBe(100); 240 }); 241 242 it("should show info when only maintenance exists", () => { 243 const data = [createStatusData(0, 100, 0, 0)]; 244 const events = [createMaintenance(1, 0)]; 245 246 const result = setDataByType({ 247 events, 248 data, 249 cardType: "requests", 250 barType: "dominant", 251 }); 252 253 expect(result[0].bar).toHaveLength(1); 254 expect(result[0].bar[0].status).toBe("info"); 255 expect(result[0].bar[0].height).toBe(100); 256 }); 257 258 it("should prioritize error over other statuses", () => { 259 const data = [createStatusData(0, 100, 0, 0)]; 260 const events = [ 261 createIncident(1, 0), 262 createReport(2, 0), 263 createMaintenance(3, 0), 264 ]; 265 266 const result = setDataByType({ 267 events, 268 data, 269 cardType: "requests", 270 barType: "dominant", 271 }); 272 273 expect(result[0].bar[0].status).toBe("error"); 274 }); 275 276 it("should show data status when no events", () => { 277 const data = [createStatusData(0, 0, 100, 0)]; 278 const events: Event[] = []; 279 280 const result = setDataByType({ 281 events, 282 data, 283 cardType: "requests", 284 barType: "dominant", 285 }); 286 287 expect(result[0].bar[0].status).toBe("degraded"); 288 }); 289 }); 290 291 describe("barType: manual", () => { 292 it("should show degraded when reports exist", () => { 293 const data = [createStatusData(0, 100, 0, 0)]; 294 const events = [createReport(1, 0)]; 295 296 const result = setDataByType({ 297 events, 298 data, 299 cardType: "manual", 300 barType: "manual", 301 }); 302 303 expect(result[0].bar).toHaveLength(1); 304 expect(result[0].bar[0].status).toBe("degraded"); 305 expect(result[0].bar[0].height).toBe(100); 306 }); 307 308 it("should show info when only maintenance exists", () => { 309 const data = [createStatusData(0, 100, 0, 0)]; 310 const events = [createMaintenance(1, 0)]; 311 312 const result = setDataByType({ 313 events, 314 data, 315 cardType: "manual", 316 barType: "manual", 317 }); 318 319 expect(result[0].bar).toHaveLength(1); 320 expect(result[0].bar[0].status).toBe("info"); 321 expect(result[0].bar[0].height).toBe(100); 322 }); 323 324 it("should ignore incidents and show success", () => { 325 const data = [createStatusData(0, 100, 0, 0)]; 326 const events = [createIncident(1, 0)]; 327 328 const result = setDataByType({ 329 events, 330 data, 331 cardType: "manual", 332 barType: "manual", 333 }); 334 335 expect(result[0].bar).toHaveLength(1); 336 expect(result[0].bar[0].status).toBe("success"); 337 }); 338 339 it("should prioritize reports over maintenance", () => { 340 const data = [createStatusData(0, 100, 0, 0)]; 341 const events = [createReport(1, 0), createMaintenance(2, 0)]; 342 343 const result = setDataByType({ 344 events, 345 data, 346 cardType: "manual", 347 barType: "manual", 348 }); 349 350 expect(result[0].bar[0].status).toBe("degraded"); 351 }); 352 }); 353 354 describe("cardType: requests", () => { 355 it("should show request counts for each status", () => { 356 const data = [createStatusData(0, 100, 50, 10)]; 357 const events: Event[] = []; 358 359 const result = setDataByType({ 360 events, 361 data, 362 cardType: "requests", 363 barType: "dominant", 364 }); 365 366 expect(result[0].card.length).toBe(3); 367 expect(result[0].card.some((c) => c.value.includes("100 reqs"))).toBe( 368 true, 369 ); 370 expect(result[0].card.some((c) => c.value.includes("50 reqs"))).toBe( 371 true, 372 ); 373 expect(result[0].card.some((c) => c.value.includes("10 reqs"))).toBe( 374 true, 375 ); 376 }); 377 378 it("should format large numbers correctly", () => { 379 const data = [createStatusData(0, 5000, 0, 0)]; 380 const events: Event[] = []; 381 382 const result = setDataByType({ 383 events, 384 data, 385 cardType: "requests", 386 barType: "dominant", 387 }); 388 389 expect(result[0].card[0].value).toBe("5.0k reqs"); 390 }); 391 392 it("should show empty card when no data", () => { 393 const data = [createStatusData(0, 0, 0, 0)]; 394 const events: Event[] = []; 395 396 const result = setDataByType({ 397 events, 398 data, 399 cardType: "requests", 400 barType: "dominant", 401 }); 402 403 expect(result[0].card).toHaveLength(1); 404 expect(result[0].card[0].value).toBe(""); 405 expect(result[0].card[0].status).toBe("empty"); 406 }); 407 408 it("should show event status in empty card when no data but events exist", () => { 409 const data = [createStatusData(0, 0, 0, 0)]; 410 const events = [createIncident(1, 0)]; 411 412 const result = setDataByType({ 413 events, 414 data, 415 cardType: "requests", 416 barType: "dominant", 417 }); 418 419 expect(result[0].card).toHaveLength(1); 420 expect(result[0].card[0].status).toBe("error"); 421 }); 422 }); 423 424 describe("cardType: duration", () => { 425 it("should calculate duration for events", () => { 426 const data = [createStatusData(0, 100, 0, 0)]; 427 const events = [ 428 createIncident(1, 0, 1), // 1 hour 429 createReport(2, 0, 2), // 2 hours 430 ]; 431 432 const result = setDataByType({ 433 events, 434 data, 435 cardType: "duration", 436 barType: "absolute", 437 }); 438 439 expect(result[0].card.length).toBeGreaterThan(0); 440 // Should have durations for error, degraded, and success 441 const hasError = result[0].card.some( 442 (c) => c.status === "error" && c.value.includes("h"), 443 ); 444 const hasDegraded = result[0].card.some( 445 (c) => c.status === "degraded" && c.value.includes("h"), 446 ); 447 expect(hasError || hasDegraded).toBe(true); 448 }); 449 450 it("should format duration in hours and minutes", () => { 451 const data = [createStatusData(0, 100, 0, 0)]; 452 const events = [createIncident(1, 0, 1.5)]; // 1.5 hours = 1h 30m 453 454 const result = setDataByType({ 455 events, 456 data, 457 cardType: "duration", 458 barType: "absolute", 459 }); 460 461 const errorCard = result[0].card.find((c) => c.status === "error"); 462 expect(errorCard).toBeDefined(); 463 // Should contain hour notation and optionally minutes 464 expect(errorCard?.value).toMatch(/\d+h(\s\d+m)?/); 465 }); 466 467 it("should show success duration as remaining time", () => { 468 const data = [createStatusData(0, 100, 0, 0)]; 469 const events = [createIncident(1, 0, 1)]; // 1 hour downtime 470 471 const result = setDataByType({ 472 events, 473 data, 474 cardType: "duration", 475 barType: "absolute", 476 }); 477 478 const successCard = result[0].card.find((c) => c.status === "success"); 479 expect(successCard).toBeDefined(); 480 // Success duration should be total time minus downtime 481 expect(successCard?.value).toBeTruthy(); 482 }); 483 484 it("should exclude maintenance from success calculation", () => { 485 const data = [createStatusData(0, 100, 0, 0)]; 486 const events = [createMaintenance(1, 0, 2)]; // 2 hours maintenance 487 488 const result = setDataByType({ 489 events, 490 data, 491 cardType: "duration", 492 barType: "absolute", 493 }); 494 495 const successCard = result[0].card.find((c) => c.status === "success"); 496 // Success should account for maintenance being excluded from total time 497 expect(successCard).toBeDefined(); 498 }); 499 500 it("should show empty card when no data", () => { 501 const data = [createStatusData(0, 0, 0, 0)]; 502 const events: Event[] = []; 503 504 const result = setDataByType({ 505 events, 506 data, 507 cardType: "duration", 508 barType: "absolute", 509 }); 510 511 expect(result[0].card).toHaveLength(1); 512 expect(result[0].card[0].value).toBe(""); 513 }); 514 }); 515 516 describe("cardType: dominant", () => { 517 it("should show dominant status without value", () => { 518 const data = [createStatusData(0, 100, 0, 0)]; 519 const events = [createIncident(1, 0)]; 520 521 const result = setDataByType({ 522 events, 523 data, 524 cardType: "dominant", 525 barType: "dominant", 526 }); 527 528 expect(result[0].card).toHaveLength(1); 529 expect(result[0].card[0].status).toBe("error"); 530 expect(result[0].card[0].value).toBe(""); 531 }); 532 }); 533 534 describe("cardType: manual", () => { 535 it("should show degraded for reports", () => { 536 const data = [createStatusData(0, 100, 0, 0)]; 537 const events = [createReport(1, 0)]; 538 539 const result = setDataByType({ 540 events, 541 data, 542 cardType: "manual", 543 barType: "manual", 544 }); 545 546 expect(result[0].card).toHaveLength(1); 547 expect(result[0].card[0].status).toBe("degraded"); 548 expect(result[0].card[0].value).toBe(""); 549 }); 550 551 it("should show success when no manual events", () => { 552 const data = [createStatusData(0, 100, 0, 0)]; 553 const events = [createIncident(1, 0)]; 554 555 const result = setDataByType({ 556 events, 557 data, 558 cardType: "manual", 559 barType: "manual", 560 }); 561 562 expect(result[0].card[0].status).toBe("success"); 563 }); 564 }); 565 566 describe("event bundling", () => { 567 it("should bundle more than 4 incidents into single event", () => { 568 const data = [createStatusData(0, 100, 0, 0)]; 569 const events = [ 570 createIncident(1, 0), 571 createIncident(2, 0), 572 createIncident(3, 0), 573 createIncident(4, 0), 574 createIncident(5, 0), 575 ]; 576 577 const result = setDataByType({ 578 events, 579 data, 580 cardType: "requests", 581 barType: "absolute", 582 }); 583 584 // Should have bundled incident with special id -1 585 const bundledIncident = result[0].events.find((e) => e.id === -1); 586 expect(bundledIncident).toBeDefined(); 587 expect(bundledIncident?.name).toContain("5 incidents"); 588 }); 589 590 it("should not bundle 4 or fewer incidents", () => { 591 const data = [createStatusData(0, 100, 0, 0)]; 592 const events = [ 593 createIncident(1, 0), 594 createIncident(2, 0), 595 createIncident(3, 0), 596 createIncident(4, 0), 597 ]; 598 599 const result = setDataByType({ 600 events, 601 data, 602 cardType: "requests", 603 barType: "absolute", 604 }); 605 606 // Should not have bundled incident 607 const bundledIncident = result[0].events.find((e) => e.id === -1); 608 expect(bundledIncident).toBeUndefined(); 609 }); 610 611 it("should not bundle incidents for non-absolute bar types", () => { 612 const data = [createStatusData(0, 100, 0, 0)]; 613 const events = [ 614 createIncident(1, 0), 615 createIncident(2, 0), 616 createIncident(3, 0), 617 createIncident(4, 0), 618 createIncident(5, 0), 619 ]; 620 621 const result = setDataByType({ 622 events, 623 data, 624 cardType: "requests", 625 barType: "dominant", 626 }); 627 628 // Should not include any incidents in events array 629 expect(result[0].events.length).toBe(0); 630 }); 631 }); 632 633 describe("multiple days", () => { 634 it("should handle data across multiple days", () => { 635 const data = [ 636 createStatusData(0, 100, 0, 0), 637 createStatusData(1, 80, 20, 0), 638 createStatusData(2, 60, 30, 10), 639 ]; 640 const events = [ 641 createIncident(1, 0), 642 createReport(2, 1), 643 createMaintenance(3, 2), 644 ]; 645 646 const result = setDataByType({ 647 events, 648 data, 649 cardType: "requests", 650 barType: "dominant", 651 }); 652 653 expect(result).toHaveLength(3); 654 expect(result[0].bar[0].status).toBe("error"); // Day 0 has incident 655 expect(result[1].bar[0].status).toBe("degraded"); // Day 1 has report 656 expect(result[2].bar[0].status).toBe("info"); // Day 2 has maintenance 657 }); 658 }); 659 660 describe("edge cases", () => { 661 it("should handle empty data array", () => { 662 const data: StatusData[] = []; 663 const events: Event[] = []; 664 665 const result = setDataByType({ 666 events, 667 data, 668 cardType: "requests", 669 barType: "dominant", 670 }); 671 672 expect(result).toHaveLength(0); 673 }); 674 675 it("should handle events with null end date", () => { 676 const data = [createStatusData(0, 100, 0, 0)]; 677 const events: Event[] = [ 678 { 679 id: 1, 680 name: "Ongoing Incident", 681 from: new Date(), 682 to: null, 683 type: "incident", 684 status: "error", 685 }, 686 ]; 687 688 const result = setDataByType({ 689 events, 690 data, 691 cardType: "duration", 692 barType: "absolute", 693 }); 694 695 expect(result[0].bar.some((b) => b.status === "error")).toBe(true); 696 }); 697 698 it("should handle events spanning multiple days", () => { 699 const data = [ 700 createStatusData(0, 100, 0, 0), 701 createStatusData(1, 100, 0, 0), 702 ]; 703 const from = new Date(); 704 from.setDate(from.getDate() - 1); 705 from.setHours(12, 0, 0, 0); 706 const to = new Date(); 707 to.setHours(12, 0, 0, 0); 708 709 const events: Event[] = [ 710 { 711 id: 1, 712 name: "Multi-day Incident", 713 from, 714 to, 715 type: "incident", 716 status: "error", 717 }, 718 ]; 719 720 const result = setDataByType({ 721 events, 722 data, 723 cardType: "requests", 724 barType: "dominant", 725 }); 726 727 // Both days should show error status 728 expect(result[0].bar[0].status).toBe("error"); 729 expect(result[1].bar[0].status).toBe("error"); 730 }); 731 }); 732}); 733 734describe("fillStatusDataFor45Days", () => { 735 it("should fill all 45 days", () => { 736 const data: StatusData[] = []; 737 const result = fillStatusDataFor45Days(data, "1"); 738 739 expect(result).toHaveLength(45); 740 }); 741 742 it("should sort data by day oldest first", () => { 743 const data: StatusData[] = []; 744 const result = fillStatusDataFor45Days(data, "1"); 745 746 for (let i = 1; i < result.length; i++) { 747 const prev = new Date(result[i - 1].day); 748 const curr = new Date(result[i].day); 749 expect(curr.getTime()).toBeGreaterThan(prev.getTime()); 750 } 751 }); 752 753 it("should preserve existing data", () => { 754 const existingData = [createStatusData(5, 100, 50, 10)]; 755 const result = fillStatusDataFor45Days(existingData, "1"); 756 757 expect(result).toHaveLength(45); 758 const matchingDay = result.find( 759 (d) => d.ok === 100 && d.degraded === 50 && d.error === 10, 760 ); 761 expect(matchingDay).toBeDefined(); 762 }); 763 764 it("should fill missing days with zeros", () => { 765 const existingData = [createStatusData(5, 100, 0, 0)]; 766 const result = fillStatusDataFor45Days(existingData, "1"); 767 768 const emptyDays = result.filter((d) => d.count === 0); 769 expect(emptyDays.length).toBe(44); 770 }); 771}); 772 773describe("getUptime", () => { 774 describe("manual bar type", () => { 775 it("should calculate uptime based on report durations", () => { 776 const data = Array.from({ length: 45 }, (_, i) => 777 createStatusData(i, 100, 0, 0), 778 ); 779 const events = [createReport(1, 0, 24)]; // 1 day downtime 780 781 const uptime = getUptime({ 782 data, 783 events, 784 barType: "manual", 785 cardType: "manual", 786 }); 787 788 // Should be approximately 97.78% (44/45 days) 789 expect(Number.parseFloat(uptime)).toBeGreaterThan(97); 790 expect(Number.parseFloat(uptime)).toBeLessThan(98); 791 }); 792 793 it("should only consider reports not incidents", () => { 794 const data = Array.from({ length: 45 }, (_, i) => 795 createStatusData(i, 100, 0, 0), 796 ); 797 const events = [createIncident(1, 0, 24)]; // Should be ignored 798 799 const uptime = getUptime({ 800 data, 801 events, 802 barType: "manual", 803 cardType: "manual", 804 }); 805 806 expect(uptime).toBe("100%"); 807 }); 808 }); 809 810 describe("duration card type", () => { 811 it("should calculate uptime based on incident durations", () => { 812 const data = Array.from({ length: 45 }, (_, i) => 813 createStatusData(i, 100, 0, 0), 814 ); 815 const events = [createIncident(1, 0, 24)]; // 1 day downtime 816 817 const uptime = getUptime({ 818 data, 819 events, 820 barType: "absolute", 821 cardType: "duration", 822 }); 823 824 // Should be approximately 97.78% (44/45 days) 825 expect(Number.parseFloat(uptime)).toBeGreaterThan(97); 826 expect(Number.parseFloat(uptime)).toBeLessThan(98); 827 }); 828 it("should ignore reports when calculating duration uptime", () => { 829 const data = Array.from({ length: 45 }, (_, i) => 830 createStatusData(i, 100, 0, 0), 831 ); 832 const events = [createReport(2, 0, 24)]; // Should be ignored 833 834 const uptime = getUptime({ 835 data, 836 events, 837 barType: "absolute", 838 cardType: "duration", 839 }); 840 841 expect(uptime).toBe("100%"); 842 }); 843 }); 844 845 describe("request card type", () => { 846 it("should calculate uptime based on ok vs total requests", () => { 847 const data = [ 848 createStatusData(0, 90, 5, 5), // 95 ok, 100 total 849 createStatusData(1, 100, 0, 0), // 100 ok, 100 total 850 ]; 851 const events: Event[] = []; 852 853 const uptime = getUptime({ 854 data, 855 events, 856 barType: "absolute", 857 cardType: "requests", 858 }); 859 860 // (90+5+100) / (90+5+5+100) = 195/200 = 97.5% 861 expect(uptime).toBe("97.5%"); 862 }); 863 864 it("should count degraded as ok", () => { 865 const data = [createStatusData(0, 80, 20, 0)]; 866 const events: Event[] = []; 867 868 const uptime = getUptime({ 869 data, 870 events, 871 barType: "absolute", 872 cardType: "requests", 873 }); 874 875 expect(uptime).toBe("100%"); 876 }); 877 878 it("should return 100% for empty data", () => { 879 const data: StatusData[] = []; 880 const events: Event[] = []; 881 882 const uptime = getUptime({ 883 data, 884 events, 885 barType: "absolute", 886 cardType: "requests", 887 }); 888 889 expect(uptime).toBe("100%"); 890 }); 891 892 it("should return 100% when total is zero", () => { 893 const data = [createStatusData(0, 0, 0, 0)]; 894 const events: Event[] = []; 895 896 const uptime = getUptime({ 897 data, 898 events, 899 barType: "absolute", 900 cardType: "requests", 901 }); 902 903 expect(uptime).toBe("100%"); 904 }); 905 }); 906}); 907 908describe("getEvents - pageComponent filtering", () => { 909 // Helper to create a mock page component 910 function createMockPageComponent( 911 id: number, 912 monitorId?: number, 913 ): PageComponent { 914 return { 915 id, 916 workspaceId: 1, 917 pageId: 1, 918 type: monitorId ? ("monitor" as const) : ("static" as const), 919 monitorId: monitorId ?? null, 920 name: `Component ${id}`, 921 description: null, 922 order: 0, 923 groupId: null, 924 groupOrder: 0, 925 createdAt: new Date(), 926 updatedAt: new Date(), 927 }; 928 } 929 930 // Helper to create a mock maintenance 931 function createMockMaintenance( 932 id: number, 933 pageComponentIds: number[], 934 ): Maintenance & { 935 maintenancesToPageComponents: { 936 pageComponent: PageComponent | null; 937 }[]; 938 } { 939 const now = new Date(); 940 const from = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago 941 const to = new Date(now.getTime() + 1000 * 60 * 60); // 1 hour from now 942 943 return { 944 id, 945 title: `Maintenance ${id}`, 946 message: "Test maintenance", 947 from, 948 to, 949 workspaceId: 1, 950 pageId: 1, 951 createdAt: new Date(), 952 updatedAt: new Date(), 953 maintenancesToPageComponents: pageComponentIds.map((pcId) => ({ 954 pageComponent: createMockPageComponent(pcId, pcId * 10), 955 })), 956 }; 957 } 958 959 // Helper to create a mock status report 960 function createMockStatusReport( 961 id: number, 962 pageComponentIds: number[], 963 status: "investigating" | "resolved" = "investigating", 964 ): StatusReport & { 965 statusReportsToPageComponents: { 966 pageComponent: PageComponent | null; 967 }[]; 968 statusReportUpdates: StatusReportUpdate[]; 969 } { 970 const now = new Date(); 971 const updateDate = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago 972 973 return { 974 id, 975 title: `Status Report ${id}`, 976 status, 977 workspaceId: 1, 978 pageId: 1, 979 createdAt: new Date(), 980 updatedAt: new Date(), 981 statusReportsToPageComponents: pageComponentIds.map((pcId) => ({ 982 pageComponent: createMockPageComponent(pcId, pcId * 10), 983 })), 984 statusReportUpdates: [ 985 { 986 id: id * 100, 987 statusReportId: id, 988 date: updateDate, 989 status: "investigating", 990 message: "Investigating the issue", 991 createdAt: new Date(), 992 updatedAt: new Date(), 993 }, 994 ], 995 }; 996 } 997 998 // Helper to create a mock incident 999 function createMockIncident(id: number, monitorId: number): Incident { 1000 const now = new Date(); 1001 const startedAt = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago 1002 1003 return { 1004 id, 1005 title: `Incident ${id}`, 1006 summary: "Test incident", 1007 status: "investigating", 1008 monitorId, 1009 workspaceId: 1, 1010 startedAt, 1011 acknowledgedAt: null, 1012 acknowledgedBy: null, 1013 resolvedAt: null, 1014 resolvedBy: null, 1015 incidentScreenshotUrl: null, 1016 recoveryScreenshotUrl: null, 1017 autoResolved: false, 1018 createdAt: startedAt, 1019 updatedAt: new Date(), 1020 }; 1021 } 1022 1023 it("should filter maintenances by pageComponentId", () => { 1024 const maintenances = [ 1025 createMockMaintenance(1, [1, 2]), 1026 createMockMaintenance(2, [3, 4]), 1027 createMockMaintenance(3, [1, 5]), 1028 ]; 1029 1030 const events = getEvents({ 1031 maintenances, 1032 incidents: [], 1033 reports: [], 1034 pageComponentId: 1, 1035 pastDays: 365, 1036 }); 1037 1038 const maintenanceEvents = events.filter((e) => e.type === "maintenance"); 1039 expect(maintenanceEvents).toHaveLength(2); 1040 expect(maintenanceEvents.map((e) => e.id).sort()).toEqual([1, 3]); 1041 }); 1042 1043 it("should filter status reports by pageComponentId", () => { 1044 const reports = [ 1045 createMockStatusReport(1, [1, 2]), 1046 createMockStatusReport(2, [3, 4]), 1047 createMockStatusReport(3, [1, 5]), 1048 ]; 1049 1050 const events = getEvents({ 1051 maintenances: [], 1052 incidents: [], 1053 reports, 1054 pageComponentId: 1, 1055 pastDays: 365, 1056 }); 1057 1058 const reportEvents = events.filter((e) => e.type === "report"); 1059 expect(reportEvents).toHaveLength(2); 1060 expect(reportEvents.map((e) => e.id).sort()).toEqual([1, 3]); 1061 }); 1062 1063 it("should exclude incidents for static components", () => { 1064 const incidents = [createMockIncident(1, 10), createMockIncident(2, 20)]; 1065 1066 const events = getEvents({ 1067 maintenances: [], 1068 incidents, 1069 reports: [], 1070 componentType: "static", 1071 pastDays: 365, 1072 }); 1073 1074 const incidentEvents = events.filter((e) => e.type === "incident"); 1075 expect(incidentEvents).toHaveLength(0); 1076 }); 1077 1078 it("should include incidents for monitor components", () => { 1079 const incidents = [createMockIncident(1, 10), createMockIncident(2, 20)]; 1080 1081 const events = getEvents({ 1082 maintenances: [], 1083 incidents, 1084 reports: [], 1085 monitorId: 10, 1086 componentType: "monitor", 1087 pastDays: 365, 1088 }); 1089 1090 const incidentEvents = events.filter((e) => e.type === "incident"); 1091 expect(incidentEvents).toHaveLength(1); 1092 expect(incidentEvents[0].id).toBe(1); 1093 }); 1094 1095 it("should maintain backward compatibility with monitorId filtering", () => { 1096 const maintenances = [ 1097 createMockMaintenance(1, [1]), 1098 createMockMaintenance(2, [2]), 1099 ]; 1100 1101 const events = getEvents({ 1102 maintenances, 1103 incidents: [], 1104 reports: [], 1105 monitorId: 10, 1106 pastDays: 365, 1107 }); 1108 1109 const maintenanceEvents = events.filter((e) => e.type === "maintenance"); 1110 expect(maintenanceEvents).toHaveLength(1); 1111 expect(maintenanceEvents[0].id).toBe(1); 1112 }); 1113 1114 it("should prioritize pageComponentId over monitorId", () => { 1115 const maintenances = [ 1116 createMockMaintenance(1, [1]), 1117 createMockMaintenance(2, [2]), 1118 ]; 1119 1120 const events = getEvents({ 1121 maintenances, 1122 incidents: [], 1123 reports: [], 1124 pageComponentId: 1, 1125 monitorId: 20, 1126 pastDays: 365, 1127 }); 1128 1129 const maintenanceEvents = events.filter((e) => e.type === "maintenance"); 1130 expect(maintenanceEvents).toHaveLength(1); 1131 expect(maintenanceEvents[0].id).toBe(1); 1132 }); 1133 1134 it("should return success status for resolved reports", () => { 1135 const reports = [createMockStatusReport(1, [1], "resolved")]; 1136 1137 const events = getEvents({ 1138 maintenances: [], 1139 incidents: [], 1140 reports, 1141 pageComponentId: 1, 1142 pastDays: 365, 1143 }); 1144 1145 const reportEvents = events.filter((e) => e.type === "report"); 1146 expect(reportEvents).toHaveLength(1); 1147 expect(reportEvents[0].status).toBe("success"); 1148 }); 1149 1150 it("should return degraded status for unresolved reports", () => { 1151 const reports = [createMockStatusReport(1, [1], "investigating")]; 1152 1153 const events = getEvents({ 1154 maintenances: [], 1155 incidents: [], 1156 reports, 1157 pageComponentId: 1, 1158 pastDays: 365, 1159 }); 1160 1161 const reportEvents = events.filter((e) => e.type === "report"); 1162 expect(reportEvents).toHaveLength(1); 1163 expect(reportEvents[0].status).toBe("degraded"); 1164 }); 1165});