Openstatus
www.openstatus.dev
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});