@oneuptime/common 12.0.28 → 12.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  3. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  4. package/Server/API/DashboardDomainAPI.ts +74 -0
  5. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  6. package/Server/API/StatusPageDomainAPI.ts +74 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  11. package/Server/Services/DashboardDomainService.ts +131 -0
  12. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  13. package/Server/Services/MonitorTemplateService.ts +70 -11
  14. package/Server/Services/StatusPageDomainService.ts +163 -3
  15. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  16. package/Server/Types/Domain.ts +17 -0
  17. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  18. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  19. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  20. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  21. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  22. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  23. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  24. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  25. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  26. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  27. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  28. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  29. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  30. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  31. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  32. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  33. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  34. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  35. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  36. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  37. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  38. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  39. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  40. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  41. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  42. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  43. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  44. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  45. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  46. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  47. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  48. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  49. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  50. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  51. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  52. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  53. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  54. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  55. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  56. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  57. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  58. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  59. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  60. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  61. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  62. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  63. package/Tests/Types/API/Route.test.ts +36 -4
  64. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  65. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  66. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  67. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  68. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  69. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  70. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  71. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  72. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  73. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  74. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  75. package/Tests/Utils/Array.test.ts +100 -0
  76. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  77. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  79. package/Types/API/Route.ts +20 -5
  80. package/Types/Date.ts +143 -13
  81. package/Types/Rum/SessionReplay.ts +62 -1
  82. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  83. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  84. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  85. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  86. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  87. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  88. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  89. package/UI/Utils/Navigation.ts +42 -0
  90. package/Utils/Array.ts +37 -0
  91. package/Utils/CertificateReissue.ts +128 -0
  92. package/Utils/Grok/Grok.ts +461 -0
  93. package/Utils/Grok/GrokPatterns.ts +118 -0
  94. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  95. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  96. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  98. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  100. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  101. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  102. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  103. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  104. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  105. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  106. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  114. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  115. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  116. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  117. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  118. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  119. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  120. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  122. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  123. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  124. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  125. package/build/dist/Server/Types/Domain.js +16 -0
  126. package/build/dist/Server/Types/Domain.js.map +1 -1
  127. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  128. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  129. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  130. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  131. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  132. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  133. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  134. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  135. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  136. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  137. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  138. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  139. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  142. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  143. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  144. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  145. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  146. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  147. package/build/dist/Types/API/Route.js +16 -5
  148. package/build/dist/Types/API/Route.js.map +1 -1
  149. package/build/dist/Types/Date.js +115 -13
  150. package/build/dist/Types/Date.js.map +1 -1
  151. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  152. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  153. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  154. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  155. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  157. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  158. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  159. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  160. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  161. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  162. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  163. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  164. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  165. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  166. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  167. package/build/dist/UI/Utils/Navigation.js +30 -0
  168. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  169. package/build/dist/Utils/Array.js +25 -0
  170. package/build/dist/Utils/Array.js.map +1 -1
  171. package/build/dist/Utils/CertificateReissue.js +97 -0
  172. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  173. package/build/dist/Utils/Grok/Grok.js +324 -0
  174. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  175. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  176. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  178. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  179. package/package.json +1 -1
  180. package/UI/Utils/JsonWebToken.ts +0 -14
  181. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  182. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import ArrayUtil from "../../Utils/Array";
2
2
  import ObjectID from "../../Types/ObjectID";
3
+ import Sleep from "../../Types/Sleep";
3
4
 
4
5
  describe("ArrayUtil", () => {
5
6
  describe("removeDuplicates", () => {
@@ -153,4 +154,103 @@ describe("ArrayUtil", () => {
153
154
  expect(ArrayUtil.distinctByFieldName([], "group")).toEqual([]);
154
155
  });
155
156
  });
157
+ describe("forEachWithConcurrency", () => {
158
+ it("runs every item exactly once", async () => {
159
+ const items: Array<number> = [1, 2, 3, 4, 5, 6, 7];
160
+ const seen: Array<number> = [];
161
+
162
+ await ArrayUtil.forEachWithConcurrency(
163
+ items,
164
+ 3,
165
+ async (item: number): Promise<void> => {
166
+ seen.push(item);
167
+ },
168
+ );
169
+
170
+ expect(seen.sort()).toEqual(items);
171
+ });
172
+
173
+ it("never exceeds the requested concurrency", async () => {
174
+ let inFlight: number = 0;
175
+ let peak: number = 0;
176
+
177
+ await ArrayUtil.forEachWithConcurrency(
178
+ Array.from({ length: 30 }, (_v: unknown, i: number) => {
179
+ return i;
180
+ }),
181
+ 4,
182
+ async (): Promise<void> => {
183
+ inFlight++;
184
+ peak = Math.max(peak, inFlight);
185
+ await Sleep.sleep(1);
186
+ inFlight--;
187
+ },
188
+ );
189
+
190
+ expect(peak).toBeLessThanOrEqual(4);
191
+ });
192
+
193
+ it("actually overlaps work rather than awaiting one at a time", async () => {
194
+ let inFlight: number = 0;
195
+ let peak: number = 0;
196
+
197
+ await ArrayUtil.forEachWithConcurrency(
198
+ Array.from({ length: 10 }, (_v: unknown, i: number) => {
199
+ return i;
200
+ }),
201
+ 5,
202
+ async (): Promise<void> => {
203
+ inFlight++;
204
+ peak = Math.max(peak, inFlight);
205
+ await Sleep.sleep(5);
206
+ inFlight--;
207
+ },
208
+ );
209
+
210
+ expect(peak).toBeGreaterThan(1);
211
+ });
212
+
213
+ it("starts items in order, so a caller that sorted by urgency is respected", async () => {
214
+ const started: Array<number> = [];
215
+
216
+ await ArrayUtil.forEachWithConcurrency(
217
+ [0, 1, 2, 3, 4, 5],
218
+ 2,
219
+ async (item: number): Promise<void> => {
220
+ started.push(item);
221
+ await Sleep.sleep(1);
222
+ },
223
+ );
224
+
225
+ expect(started.slice(0, 2)).toEqual([0, 1]);
226
+ });
227
+
228
+ it("does no work and does not hang on an empty array", async () => {
229
+ let calls: number = 0;
230
+
231
+ await ArrayUtil.forEachWithConcurrency(
232
+ [] as Array<number>,
233
+ 5,
234
+ async (): Promise<void> => {
235
+ calls++;
236
+ },
237
+ );
238
+
239
+ expect(calls).toBe(0);
240
+ });
241
+
242
+ it("treats a concurrency below one as one rather than doing nothing", async () => {
243
+ const seen: Array<number> = [];
244
+
245
+ await ArrayUtil.forEachWithConcurrency(
246
+ [1, 2, 3],
247
+ 0,
248
+ async (item: number): Promise<void> => {
249
+ seen.push(item);
250
+ },
251
+ );
252
+
253
+ expect(seen).toEqual([1, 2, 3]);
254
+ });
255
+ });
156
256
  });
@@ -0,0 +1,225 @@
1
+ import CertificateReissueUtil from "../../Utils/CertificateReissue";
2
+ import OneUptimeDate from "../../Types/Date";
3
+ import { describe, expect, test } from "@jest/globals";
4
+
5
+ /*
6
+ * The cooldown behind the dashboard's "Reissue SSL" button.
7
+ *
8
+ * Everything here is pure, and deliberately so: the API refuses a request
9
+ * using these functions and the dashboard greys the button out using the very
10
+ * same ones, so the two can never disagree about whether a domain is allowed
11
+ * to order right now. What the tests pin is the property the whole feature
12
+ * rests on - that the boundary the SERVER claims a row with
13
+ * (getCooldownCutoff, which becomes a SQL comparison) and the boundary the
14
+ * CLIENT renders (isInCooldown) are the same boundary.
15
+ */
16
+
17
+ const NOW: Date = OneUptimeDate.fromString("2026-08-31T12:00:00.000Z");
18
+
19
+ type HoursAgoFunction = (hours: number) => Date;
20
+
21
+ const hoursAgo: HoursAgoFunction = (hours: number): Date => {
22
+ return OneUptimeDate.addRemoveHours(NOW, -hours);
23
+ };
24
+
25
+ describe("CertificateReissueUtil cooldown window", () => {
26
+ test("a domain that has never been reissued is not cooling down", () => {
27
+ expect(CertificateReissueUtil.isInCooldown(null, NOW)).toBe(false);
28
+ expect(CertificateReissueUtil.isInCooldown(undefined, NOW)).toBe(false);
29
+ });
30
+
31
+ test("a reissue requested a moment ago is cooling down", () => {
32
+ expect(CertificateReissueUtil.isInCooldown(hoursAgo(0), NOW)).toBe(true);
33
+ });
34
+
35
+ test("a reissue requested just inside the window is still cooling down", () => {
36
+ expect(
37
+ CertificateReissueUtil.isInCooldown(
38
+ hoursAgo(CertificateReissueUtil.COOLDOWN_IN_HOURS - 1),
39
+ NOW,
40
+ ),
41
+ ).toBe(true);
42
+ });
43
+
44
+ test("a reissue requested exactly a cooldown ago is no longer cooling down", () => {
45
+ expect(
46
+ CertificateReissueUtil.isInCooldown(
47
+ hoursAgo(CertificateReissueUtil.COOLDOWN_IN_HOURS),
48
+ NOW,
49
+ ),
50
+ ).toBe(false);
51
+ });
52
+
53
+ test("a reissue requested long ago is not cooling down", () => {
54
+ expect(
55
+ CertificateReissueUtil.isInCooldown(
56
+ hoursAgo(CertificateReissueUtil.COOLDOWN_IN_HOURS * 10),
57
+ NOW,
58
+ ),
59
+ ).toBe(false);
60
+ });
61
+
62
+ /*
63
+ * A stamp in the future means a clock moved backwards, not that the caller
64
+ * has earned a free order. Treating it as "not cooling down" would turn a
65
+ * clock skew into an unthrottled button.
66
+ */
67
+ test("a stamp in the future is treated as cooling down", () => {
68
+ expect(
69
+ CertificateReissueUtil.isInCooldown(
70
+ OneUptimeDate.addRemoveHours(NOW, 6),
71
+ NOW,
72
+ ),
73
+ ).toBe(true);
74
+ });
75
+
76
+ test("the cooldown is at least a day, so the button cannot be a spam vector", () => {
77
+ expect(CertificateReissueUtil.COOLDOWN_IN_HOURS).toBeGreaterThanOrEqual(24);
78
+ });
79
+ });
80
+
81
+ describe("CertificateReissueUtil.getCooldownCutoff", () => {
82
+ /*
83
+ * This is the regression that matters most. getCooldownCutoff becomes the
84
+ * `certificateReissueRequestedAt <= :cutoff OR IS NULL` clause the server
85
+ * claims a row with; isInCooldown is what the dashboard renders. If the two
86
+ * ever drift apart, one of them is lying to the customer - either a button
87
+ * that looks live and is refused, or one that looks dead and would work.
88
+ */
89
+ test("the cutoff agrees with isInCooldown at every hour across the window", () => {
90
+ for (
91
+ let hours: number = 0;
92
+ hours <= CertificateReissueUtil.COOLDOWN_IN_HOURS * 2;
93
+ hours++
94
+ ) {
95
+ const requestedAt: Date = hoursAgo(hours);
96
+
97
+ const claimableByServer: boolean = !OneUptimeDate.isAfter(
98
+ requestedAt,
99
+ CertificateReissueUtil.getCooldownCutoff(NOW),
100
+ );
101
+
102
+ const coolingDownForClient: boolean = CertificateReissueUtil.isInCooldown(
103
+ requestedAt,
104
+ NOW,
105
+ );
106
+
107
+ expect(claimableByServer).toBe(!coolingDownForClient);
108
+ }
109
+ });
110
+
111
+ test("the cutoff is exactly one cooldown behind now", () => {
112
+ expect(
113
+ OneUptimeDate.getHoursBetweenTwoDates(
114
+ CertificateReissueUtil.getCooldownCutoff(NOW),
115
+ NOW,
116
+ ),
117
+ ).toBe(CertificateReissueUtil.COOLDOWN_IN_HOURS);
118
+ });
119
+ });
120
+
121
+ describe("CertificateReissueUtil.getNextReissueAllowedAt", () => {
122
+ test("is one cooldown after the request", () => {
123
+ const requestedAt: Date = hoursAgo(3);
124
+
125
+ expect(
126
+ OneUptimeDate.getHoursBetweenTwoDates(
127
+ requestedAt,
128
+ CertificateReissueUtil.getNextReissueAllowedAt(requestedAt),
129
+ ),
130
+ ).toBe(CertificateReissueUtil.COOLDOWN_IN_HOURS);
131
+ });
132
+ });
133
+
134
+ describe("CertificateReissueUtil.getTimeRemainingText", () => {
135
+ test("reports whole hours and minutes", () => {
136
+ /*
137
+ * Requested 21h30m ago, so 2h30m of a 24 hour cooldown is left.
138
+ */
139
+ const requestedAt: Date = OneUptimeDate.addRemoveHours(
140
+ OneUptimeDate.fromString("2026-08-30T14:30:00.000Z"),
141
+ 0,
142
+ );
143
+
144
+ expect(CertificateReissueUtil.getTimeRemainingText(requestedAt, NOW)).toBe(
145
+ "2 hours 30 minutes",
146
+ );
147
+ });
148
+
149
+ test("singularizes a lone hour and a lone minute", () => {
150
+ expect(
151
+ CertificateReissueUtil.getTimeRemainingText(
152
+ OneUptimeDate.addRemoveHours(
153
+ NOW,
154
+ -(CertificateReissueUtil.COOLDOWN_IN_HOURS - 1),
155
+ ),
156
+ NOW,
157
+ ),
158
+ ).toBe("1 hour");
159
+
160
+ expect(
161
+ CertificateReissueUtil.getTimeRemainingText(
162
+ OneUptimeDate.addRemoveMinutes(
163
+ NOW,
164
+ -(CertificateReissueUtil.COOLDOWN_IN_HOURS * 60 - 1),
165
+ ),
166
+ NOW,
167
+ ),
168
+ ).toBe("1 minute");
169
+ });
170
+
171
+ test("omits an hours part that is zero", () => {
172
+ const text: string = CertificateReissueUtil.getTimeRemainingText(
173
+ OneUptimeDate.addRemoveMinutes(
174
+ NOW,
175
+ -(CertificateReissueUtil.COOLDOWN_IN_HOURS * 60 - 20),
176
+ ),
177
+ NOW,
178
+ );
179
+
180
+ expect(text).toBe("20 minutes");
181
+ expect(text).not.toContain("hour");
182
+ });
183
+
184
+ /*
185
+ * Never a negative or a bare "0 minutes": once the cooldown is over the
186
+ * message is not rendered at all, and the edge of the window must read as
187
+ * an edge rather than as a countdown that overshot.
188
+ */
189
+ test("never counts below zero once the window has passed", () => {
190
+ expect(
191
+ CertificateReissueUtil.getTimeRemainingText(
192
+ hoursAgo(CertificateReissueUtil.COOLDOWN_IN_HOURS * 5),
193
+ NOW,
194
+ ),
195
+ ).toBe("less than a minute");
196
+ });
197
+ });
198
+
199
+ describe("CertificateReissueUtil.getCooldownMessage", () => {
200
+ test("tells the customer why and for how long", () => {
201
+ const message: string = CertificateReissueUtil.getCooldownMessage(
202
+ hoursAgo(1),
203
+ NOW,
204
+ );
205
+
206
+ expect(message).toContain("Let's Encrypt");
207
+ expect(message).toContain(
208
+ `${CertificateReissueUtil.COOLDOWN_IN_HOURS} hours`,
209
+ );
210
+ expect(message).toContain(
211
+ CertificateReissueUtil.getTimeRemainingText(hoursAgo(1), NOW),
212
+ );
213
+ });
214
+
215
+ test("does not leak an internal column name or id at the customer", () => {
216
+ const message: string = CertificateReissueUtil.getCooldownMessage(
217
+ hoursAgo(1),
218
+ NOW,
219
+ );
220
+
221
+ expect(message).not.toContain("certificateReissueRequestedAt");
222
+ expect(message).not.toContain("undefined");
223
+ expect(message).not.toContain("NaN");
224
+ });
225
+ });