@oneuptime/common 12.0.28 → 12.0.30

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 (367) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/Index.ts +11 -0
  3. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  4. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  6. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  7. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  8. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  9. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  10. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  11. package/Server/API/DashboardDomainAPI.ts +74 -0
  12. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  13. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  14. package/Server/API/StatusPageDomainAPI.ts +74 -0
  15. package/Server/EnvironmentConfig.ts +138 -0
  16. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  21. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  22. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  23. package/Server/Services/DashboardDomainService.ts +131 -0
  24. package/Server/Services/Index.ts +12 -0
  25. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  26. package/Server/Services/MonitorTemplateService.ts +70 -11
  27. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  28. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  29. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  30. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  31. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  32. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  33. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  34. package/Server/Services/StatusPageDomainService.ts +163 -3
  35. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  36. package/Server/Services/TeamMemberService.ts +489 -1
  37. package/Server/Services/TeamService.ts +50 -0
  38. package/Server/Services/UserNotificationSettingService.ts +44 -1
  39. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  40. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  41. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  42. package/Server/Types/Domain.ts +17 -0
  43. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  44. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  45. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  46. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  47. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  48. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  49. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  50. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  51. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  52. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  53. package/Server/Utils/Response.ts +134 -0
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  55. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  56. package/Server/Utils/StartServer.ts +10 -0
  57. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  60. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  61. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  63. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  64. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  65. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  66. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  67. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  68. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  69. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  70. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  71. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  72. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  73. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  74. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  75. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  76. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  77. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  78. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  79. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  80. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  81. package/Tests/Server/API/Helpers.ts +8 -0
  82. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  83. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  84. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  85. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  86. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  87. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  88. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  89. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  90. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  91. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  92. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  93. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  94. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  95. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  96. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  97. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  98. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  99. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  100. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  101. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  102. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  103. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  104. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  105. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  106. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  107. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  108. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  109. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  110. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  111. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  112. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  113. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  114. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  115. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  116. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  117. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  118. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  119. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  120. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  121. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  122. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  123. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  124. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  125. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  126. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  127. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  128. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  129. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  130. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  131. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  132. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  133. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  134. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  135. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  137. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  138. package/Tests/Types/API/Route.test.ts +36 -4
  139. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  140. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  141. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  142. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  143. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  144. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  145. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  146. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  147. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  148. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  149. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  150. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  151. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  152. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  153. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  154. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  155. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  156. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  157. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  158. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  159. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  160. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  161. package/Tests/Utils/Array.test.ts +100 -0
  162. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  163. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  164. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  165. package/Types/API/Route.ts +20 -5
  166. package/Types/Calendar/ICalendar.ts +442 -0
  167. package/Types/Date.ts +143 -13
  168. package/Types/Email/EmailTemplateType.ts +2 -0
  169. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  170. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  171. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  172. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  173. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  174. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  175. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  176. package/Types/Rum/SessionReplay.ts +62 -1
  177. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  178. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  179. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  180. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  181. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  182. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  183. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  184. package/UI/Utils/Navigation.ts +42 -0
  185. package/Utils/Array.ts +37 -0
  186. package/Utils/CertificateReissue.ts +128 -0
  187. package/Utils/Grok/Grok.ts +461 -0
  188. package/Utils/Grok/GrokPatterns.ts +118 -0
  189. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  190. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  191. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  193. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  195. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  197. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  200. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  201. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  202. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  203. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  205. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  206. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  207. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  208. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  209. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  210. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  211. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  212. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  213. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  214. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  215. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  216. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  217. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  218. package/build/dist/Server/EnvironmentConfig.js +102 -0
  219. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  220. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  221. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  224. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  225. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  226. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  227. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  230. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  231. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  232. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  233. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  234. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  235. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  236. package/build/dist/Server/Services/Index.js +12 -0
  237. package/build/dist/Server/Services/Index.js.map +1 -1
  238. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  239. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  240. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  241. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  242. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  243. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  244. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  245. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  246. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  247. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  248. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  249. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  250. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  251. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  252. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  253. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  254. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  255. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  256. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  257. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  258. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  259. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  260. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  261. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  262. package/build/dist/Server/Services/TeamService.js +43 -0
  263. package/build/dist/Server/Services/TeamService.js.map +1 -1
  264. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  265. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  266. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  267. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  268. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  269. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  270. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  271. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  272. package/build/dist/Server/Types/Domain.js +16 -0
  273. package/build/dist/Server/Types/Domain.js.map +1 -1
  274. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  275. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  276. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  277. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  278. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  279. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  280. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  281. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  282. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  283. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  284. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  285. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  286. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  287. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  288. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  289. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  290. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  291. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  292. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  293. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  294. package/build/dist/Server/Utils/Response.js +97 -0
  295. package/build/dist/Server/Utils/Response.js.map +1 -1
  296. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  297. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  298. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  299. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  300. package/build/dist/Server/Utils/StartServer.js +9 -1
  301. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  302. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  303. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  304. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  314. package/build/dist/Types/API/Route.js +16 -5
  315. package/build/dist/Types/API/Route.js.map +1 -1
  316. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  317. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  318. package/build/dist/Types/Date.js +115 -13
  319. package/build/dist/Types/Date.js.map +1 -1
  320. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  321. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  322. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  323. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  324. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  325. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  326. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  327. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  328. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  329. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  330. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  331. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  332. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  333. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  334. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  335. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  336. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  337. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  338. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  339. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  340. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  341. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  342. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  343. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  344. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  345. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  346. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  347. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  348. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  349. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  350. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  351. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  352. package/build/dist/UI/Utils/Navigation.js +30 -0
  353. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  354. package/build/dist/Utils/Array.js +25 -0
  355. package/build/dist/Utils/Array.js.map +1 -1
  356. package/build/dist/Utils/CertificateReissue.js +97 -0
  357. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  358. package/build/dist/Utils/Grok/Grok.js +324 -0
  359. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  360. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  361. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  362. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  363. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  364. package/package.json +1 -1
  365. package/UI/Utils/JsonWebToken.ts +0 -14
  366. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  367. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,855 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ FeedSettings,
4
+ FeedStatus,
5
+ FeedUrls,
6
+ MyShiftsResponse,
7
+ parseFeedSettings,
8
+ parseFeedStatus,
9
+ parseFeedUrls,
10
+ parseMyShifts,
11
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedTypes";
12
+ import {
13
+ CALENDAR_FEED_DOCS_PATH,
14
+ GOOGLE_CALENDAR_ADD_URL_PREFIX,
15
+ LEAD_TIME_DAYS_COPY,
16
+ LEAD_TIME_HOURS_COPY,
17
+ LEAD_TIME_MINUTES_COPY,
18
+ LEAD_TIME_WEEKS_COPY,
19
+ LeadTimeValidation,
20
+ MY_SHIFTS_PATH,
21
+ NOTHING_FETCHED_HINT_AFTER_HOURS,
22
+ ON_CALL_CALENDAR_API_PATH,
23
+ PERSONAL_FEED_CURRENT_PATH,
24
+ PERSONAL_FEED_ROTATE_PATH,
25
+ PROJECT_FEED_CURRENT_PATH,
26
+ PROJECT_FEED_PUBLISH_PATH,
27
+ PROJECT_FEED_ROTATE_PATH,
28
+ REMINDER_PRESETS,
29
+ ReminderPreset,
30
+ ShiftDayGroup,
31
+ UPCOMING_SHIFTS_CARD_TITLE,
32
+ UPCOMING_SHIFTS_DAYS,
33
+ UpcomingShiftsWindow,
34
+ addScheduleFilter,
35
+ applyScheduleFilter,
36
+ buildGoogleAddUrl,
37
+ formatLeadTime,
38
+ getRotatedDaysAgo,
39
+ getScheduleFeedCurrentPath,
40
+ getScheduleFeedPublishPath,
41
+ getScheduleFeedRotatePath,
42
+ getUpcomingShiftsWindow,
43
+ groupShiftsByDay,
44
+ interpolate,
45
+ isCoveringShift,
46
+ shouldShowNothingFetchedHint,
47
+ translateInterpolated,
48
+ validateCustomLeadMinutes,
49
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedUtil";
50
+ import {
51
+ MAX_MINUTES_BEFORE_SHIFT,
52
+ MIN_MINUTES_BEFORE_SHIFT,
53
+ } from "../../../Models/DatabaseModels/UserOnCallShiftReminder";
54
+ import { JSONObject } from "../../../Types/JSON";
55
+ import {
56
+ DEFAULT_FUTURE_DAYS,
57
+ DEFAULT_PAST_DAYS,
58
+ } from "../../../Types/OnCallDutyPolicy/CalendarFeedWindow";
59
+ import { MaterializedShiftJson } from "../../../Types/OnCallDutyPolicy/MaterializedShift";
60
+
61
+ /*
62
+ * The pure half of the calendar-feed dashboard surfaces: route paths, URL
63
+ * shaping, the bookkeeping-line decisions, reminder lead-time rules and the
64
+ * defensive wire parsers. Everything here runs without React, which is what
65
+ * makes it cheap to pin every branch - and every branch here decides
66
+ * something a reader acts on (which link a button opens, whether a "nothing
67
+ * fetched" accusation is shown, whether a lead time is accepted).
68
+ */
69
+
70
+ const HTTPS_URL: string =
71
+ "https://oneuptime.example.com/api/on-call-calendar/user/abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG/shifts.ics";
72
+
73
+ const BASE_URLS: FeedUrls = {
74
+ https: HTTPS_URL,
75
+ webcal: HTTPS_URL.replace("https:", "webcals:"),
76
+ googleAdd: `${GOOGLE_CALENDAR_ADD_URL_PREFIX}${encodeURIComponent(HTTPS_URL)}`,
77
+ };
78
+
79
+ type MakeStatusFunction = (overrides?: Partial<FeedStatus>) => FeedStatus;
80
+
81
+ const makeStatus: MakeStatusFunction = (
82
+ overrides?: Partial<FeedStatus>,
83
+ ): FeedStatus => {
84
+ return {
85
+ exists: true,
86
+ feedId: "11111111-1111-4111-8111-111111111111",
87
+ isEnabled: true,
88
+ needsRegeneration: false,
89
+ tokenHint: "k3Qx",
90
+ rotatedAt: "2026-08-01T10:00:00.000Z",
91
+ previousTokenExpiresAt: null,
92
+ lastFetchedAt: null,
93
+ lastFetchedClient: null,
94
+ fetchCount: 0,
95
+ lastRenderTruncated: false,
96
+ settings: { pastDays: 2, futureDays: 90 },
97
+ urls: BASE_URLS,
98
+ hostWarning: null,
99
+ protocolWarning: null,
100
+ ...overrides,
101
+ };
102
+ };
103
+
104
+ type MakeShiftFunction = (
105
+ overrides: Partial<MaterializedShiftJson> & { shiftKey: string },
106
+ ) => MaterializedShiftJson;
107
+
108
+ const makeShift: MakeShiftFunction = (
109
+ overrides: Partial<MaterializedShiftJson> & { shiftKey: string },
110
+ ): MaterializedShiftJson => {
111
+ return {
112
+ contentHash: "hash",
113
+ projectId: "project-1",
114
+ scheduleId: "schedule-1",
115
+ scheduleName: "Primary",
116
+ scheduleTimezone: "Europe/Stockholm",
117
+ userId: "user-1",
118
+ userName: "Jane",
119
+ start: "2026-09-01T07:00:00.000Z",
120
+ end: "2026-09-01T15:00:00.000Z",
121
+ coverageSeconds: 8 * 3600,
122
+ policies: [],
123
+ isPast: false,
124
+ lastModifiedAt: "2026-08-30T00:00:00.000Z",
125
+ shiftConfigVersion: 3,
126
+ ...overrides,
127
+ };
128
+ };
129
+
130
+ describe("calendar feed API paths", () => {
131
+ test("every custom route hangs off the /on-call-calendar prefix", () => {
132
+ expect(ON_CALL_CALENDAR_API_PATH).toBe("/on-call-calendar");
133
+ expect(PERSONAL_FEED_CURRENT_PATH).toBe("/on-call-calendar/feed/current");
134
+ expect(PERSONAL_FEED_ROTATE_PATH).toBe("/on-call-calendar/feed/rotate");
135
+ expect(PROJECT_FEED_CURRENT_PATH).toBe(
136
+ "/on-call-calendar/project-feed/current",
137
+ );
138
+ expect(PROJECT_FEED_PUBLISH_PATH).toBe(
139
+ "/on-call-calendar/project-feed/publish",
140
+ );
141
+ expect(PROJECT_FEED_ROTATE_PATH).toBe(
142
+ "/on-call-calendar/project-feed/rotate",
143
+ );
144
+ expect(MY_SHIFTS_PATH).toBe("/on-call-calendar/my-shifts");
145
+ });
146
+
147
+ test("schedule feed paths embed the schedule id between the fixed segments", () => {
148
+ const id: string = "22222222-2222-4222-8222-222222222222";
149
+
150
+ expect(getScheduleFeedCurrentPath(id)).toBe(
151
+ `/on-call-calendar/schedule-feed/${id}/current`,
152
+ );
153
+ expect(getScheduleFeedPublishPath(id)).toBe(
154
+ `/on-call-calendar/schedule-feed/${id}/publish`,
155
+ );
156
+ expect(getScheduleFeedRotatePath(id)).toBe(
157
+ `/on-call-calendar/schedule-feed/${id}/rotate`,
158
+ );
159
+ });
160
+
161
+ test("the docs pointer is the calendar-feeds page under on-call", () => {
162
+ expect(CALENDAR_FEED_DOCS_PATH).toBe("/on-call/calendar-feeds");
163
+ });
164
+ });
165
+
166
+ describe("buildGoogleAddUrl", () => {
167
+ test("url-encodes the whole https link behind Google's cid parameter", () => {
168
+ const result: string = buildGoogleAddUrl(HTTPS_URL);
169
+
170
+ expect(
171
+ result.startsWith("https://calendar.google.com/calendar/r?cid="),
172
+ ).toBe(true);
173
+ /*
174
+ * The raw link must not survive un-encoded: its own "?" and "/" would
175
+ * break Google's parameter parsing.
176
+ */
177
+ expect(result).not.toContain(HTTPS_URL);
178
+ expect(decodeURIComponent(result.split("cid=")[1] as string)).toBe(
179
+ HTTPS_URL,
180
+ );
181
+ });
182
+ });
183
+
184
+ describe("addScheduleFilter / applyScheduleFilter", () => {
185
+ test("appends ?schedule= when the URL has no query string", () => {
186
+ expect(addScheduleFilter("https://x.example/a.ics", "sched-1")).toBe(
187
+ "https://x.example/a.ics?schedule=sched-1",
188
+ );
189
+ });
190
+
191
+ test("appends &schedule= when a query string is already there", () => {
192
+ expect(
193
+ addScheduleFilter("https://x.example/a.ics?nocache=1", "sched-1"),
194
+ ).toBe("https://x.example/a.ics?nocache=1&schedule=sched-1");
195
+ });
196
+
197
+ test("url-encodes the schedule id", () => {
198
+ expect(addScheduleFilter("https://x.example/a.ics", "a b&c")).toBe(
199
+ "https://x.example/a.ics?schedule=a%20b%26c",
200
+ );
201
+ });
202
+
203
+ test("applyScheduleFilter returns the same object when there is no schedule", () => {
204
+ expect(applyScheduleFilter(BASE_URLS, null)).toBe(BASE_URLS);
205
+ expect(applyScheduleFilter(BASE_URLS, undefined)).toBe(BASE_URLS);
206
+ expect(applyScheduleFilter(BASE_URLS, "")).toBe(BASE_URLS);
207
+ });
208
+
209
+ test("applyScheduleFilter narrows https and webcal, and rebuilds googleAdd from the narrowed https", () => {
210
+ const filtered: FeedUrls = applyScheduleFilter(BASE_URLS, "sched-9");
211
+
212
+ expect(filtered.https).toBe(`${HTTPS_URL}?schedule=sched-9`);
213
+ expect(filtered.webcal).toBe(`${BASE_URLS.webcal}?schedule=sched-9`);
214
+ expect(filtered.googleAdd).toBe(
215
+ buildGoogleAddUrl(`${HTTPS_URL}?schedule=sched-9`),
216
+ );
217
+ // The input is left untouched.
218
+ expect(BASE_URLS.https).toBe(HTTPS_URL);
219
+ });
220
+ });
221
+
222
+ describe("getRotatedDaysAgo", () => {
223
+ const now: Date = new Date("2026-08-31T12:00:00.000Z");
224
+
225
+ test("null / undefined / garbage give null rather than a number", () => {
226
+ expect(getRotatedDaysAgo(null, now)).toBeNull();
227
+ expect(getRotatedDaysAgo(undefined, now)).toBeNull();
228
+ expect(getRotatedDaysAgo("", now)).toBeNull();
229
+ expect(getRotatedDaysAgo("not a date", now)).toBeNull();
230
+ });
231
+
232
+ test("same day is 0, whole days are floored", () => {
233
+ expect(getRotatedDaysAgo("2026-08-31T08:00:00.000Z", now)).toBe(0);
234
+ expect(getRotatedDaysAgo("2026-08-30T13:00:00.000Z", now)).toBe(0);
235
+ expect(getRotatedDaysAgo("2026-08-30T11:59:00.000Z", now)).toBe(1);
236
+ expect(getRotatedDaysAgo("2026-08-01T12:00:00.000Z", now)).toBe(30);
237
+ });
238
+
239
+ test("a rotation stamped in the future (clock skew) reads as today, never negative", () => {
240
+ expect(getRotatedDaysAgo("2026-09-05T12:00:00.000Z", now)).toBe(0);
241
+ });
242
+ });
243
+
244
+ describe("shouldShowNothingFetchedHint", () => {
245
+ const rotatedAt: string = "2026-08-01T10:00:00.000Z";
246
+ const wellAfter: Date = new Date("2026-08-10T10:00:00.000Z");
247
+ const justAfter: Date = new Date(
248
+ new Date(rotatedAt).getTime() +
249
+ (NOTHING_FETCHED_HINT_AFTER_HOURS - 1) * 60 * 60 * 1000,
250
+ );
251
+ const exactly: Date = new Date(
252
+ new Date(rotatedAt).getTime() +
253
+ NOTHING_FETCHED_HINT_AFTER_HOURS * 60 * 60 * 1000,
254
+ );
255
+
256
+ test("shows once 48 hours have passed with zero fetches", () => {
257
+ expect(NOTHING_FETCHED_HINT_AFTER_HOURS).toBe(48);
258
+ expect(
259
+ shouldShowNothingFetchedHint(makeStatus({ rotatedAt }), wellAfter),
260
+ ).toBe(true);
261
+ expect(
262
+ shouldShowNothingFetchedHint(makeStatus({ rotatedAt }), exactly),
263
+ ).toBe(true);
264
+ });
265
+
266
+ test("stays quiet inside the first 48 hours", () => {
267
+ expect(
268
+ shouldShowNothingFetchedHint(makeStatus({ rotatedAt }), justAfter),
269
+ ).toBe(false);
270
+ });
271
+
272
+ test("stays quiet when anything has fetched the link", () => {
273
+ expect(
274
+ shouldShowNothingFetchedHint(
275
+ makeStatus({ rotatedAt, fetchCount: 3 }),
276
+ wellAfter,
277
+ ),
278
+ ).toBe(false);
279
+ expect(
280
+ shouldShowNothingFetchedHint(
281
+ makeStatus({ rotatedAt, lastFetchedAt: "2026-08-02T00:00:00.000Z" }),
282
+ wellAfter,
283
+ ),
284
+ ).toBe(false);
285
+ });
286
+
287
+ test("stays quiet for a missing, disabled or un-minted link", () => {
288
+ expect(
289
+ shouldShowNothingFetchedHint(
290
+ makeStatus({ rotatedAt, exists: false }),
291
+ wellAfter,
292
+ ),
293
+ ).toBe(false);
294
+ expect(
295
+ shouldShowNothingFetchedHint(
296
+ makeStatus({ rotatedAt, isEnabled: false }),
297
+ wellAfter,
298
+ ),
299
+ ).toBe(false);
300
+ expect(
301
+ shouldShowNothingFetchedHint(makeStatus({ rotatedAt: null }), wellAfter),
302
+ ).toBe(false);
303
+ expect(
304
+ shouldShowNothingFetchedHint(
305
+ makeStatus({ rotatedAt: "garbage" }),
306
+ wellAfter,
307
+ ),
308
+ ).toBe(false);
309
+ });
310
+ });
311
+
312
+ describe("reminder presets and lead times", () => {
313
+ test("the chip row is 1 week / 1 day / 1 hour / 15 min, in that order", () => {
314
+ expect(
315
+ REMINDER_PRESETS.map((preset: ReminderPreset): number => {
316
+ return preset.minutes;
317
+ }),
318
+ ).toEqual([10080, 1440, 60, 15]);
319
+ expect(
320
+ REMINDER_PRESETS.map((preset: ReminderPreset): string => {
321
+ return preset.label;
322
+ }),
323
+ ).toEqual(["1 week", "1 day", "1 hour", "15 min"]);
324
+ });
325
+
326
+ test("every preset sits inside the service's bounds", () => {
327
+ for (const preset of REMINDER_PRESETS) {
328
+ expect(preset.minutes).toBeGreaterThanOrEqual(MIN_MINUTES_BEFORE_SHIFT);
329
+ expect(preset.minutes).toBeLessThanOrEqual(MAX_MINUTES_BEFORE_SHIFT);
330
+ }
331
+ });
332
+
333
+ test("formatLeadTime uses the preset label for preset values", () => {
334
+ expect(formatLeadTime(10080)).toBe("1 week");
335
+ expect(formatLeadTime(1440)).toBe("1 day");
336
+ expect(formatLeadTime(60)).toBe("1 hour");
337
+ expect(formatLeadTime(15)).toBe("15 min");
338
+ });
339
+
340
+ test("formatLeadTime collapses exact multiples to the largest unit and nothing else", () => {
341
+ expect(formatLeadTime(20160)).toBe("2 weeks");
342
+ expect(formatLeadTime(2880)).toBe("2 days");
343
+ expect(formatLeadTime(180)).toBe("3 hours");
344
+ // 90 minutes is not "1 hour".
345
+ expect(formatLeadTime(90)).toBe("90 minutes");
346
+ expect(formatLeadTime(45)).toBe("45 minutes");
347
+ });
348
+
349
+ /*
350
+ * A custom lead time used to be the one label on the reminders card left in
351
+ * English: the four presets went through the translator and "90 minutes" did
352
+ * not. The unit sentences are whole keys with a {{count}} placeholder, so a
353
+ * language that puts the unit first can say so.
354
+ */
355
+ test("formatLeadTime translates the unit sentence, preset and non-preset alike", () => {
356
+ const dictionary: Record<string, string> = {
357
+ [LEAD_TIME_WEEKS_COPY]: "{{count}} Wochen",
358
+ [LEAD_TIME_DAYS_COPY]: "{{count}} Tage",
359
+ [LEAD_TIME_HOURS_COPY]: "{{count}} Stunden",
360
+ [LEAD_TIME_MINUTES_COPY]: "{{count}} Minuten",
361
+ "15 min": "15 Min.",
362
+ };
363
+
364
+ const translate: (value: string | undefined) => string | undefined = (
365
+ value: string | undefined,
366
+ ): string | undefined => {
367
+ return dictionary[value || ""] || value;
368
+ };
369
+
370
+ expect(formatLeadTime(90, translate)).toBe("90 Minuten");
371
+ expect(formatLeadTime(180, translate)).toBe("3 Stunden");
372
+ expect(formatLeadTime(2880, translate)).toBe("2 Tage");
373
+ expect(formatLeadTime(20160, translate)).toBe("2 Wochen");
374
+ expect(formatLeadTime(15, translate)).toBe("15 Min.");
375
+ // A locale with no entry for the sentence still renders English, not a key.
376
+ expect(
377
+ formatLeadTime(90, (): undefined => {
378
+ return undefined;
379
+ }),
380
+ ).toBe("90 minutes");
381
+ });
382
+
383
+ test("the unit sentences all carry the {{count}} placeholder the validator checks", () => {
384
+ for (const copy of [
385
+ LEAD_TIME_WEEKS_COPY,
386
+ LEAD_TIME_DAYS_COPY,
387
+ LEAD_TIME_HOURS_COPY,
388
+ LEAD_TIME_MINUTES_COPY,
389
+ ]) {
390
+ expect(copy).toContain("{{count}}");
391
+ }
392
+ });
393
+
394
+ test("validateCustomLeadMinutes accepts whole minutes inside the bounds", () => {
395
+ const ok: LeadTimeValidation = validateCustomLeadMinutes(" 90 ");
396
+ expect(ok).toEqual({ minutes: 90, error: null });
397
+ expect(validateCustomLeadMinutes(String(MIN_MINUTES_BEFORE_SHIFT))).toEqual(
398
+ {
399
+ minutes: MIN_MINUTES_BEFORE_SHIFT,
400
+ error: null,
401
+ },
402
+ );
403
+ expect(validateCustomLeadMinutes(String(MAX_MINUTES_BEFORE_SHIFT))).toEqual(
404
+ {
405
+ minutes: MAX_MINUTES_BEFORE_SHIFT,
406
+ error: null,
407
+ },
408
+ );
409
+ });
410
+
411
+ test("validateCustomLeadMinutes names the problem for empty, fractional and out-of-range input", () => {
412
+ expect(validateCustomLeadMinutes("")).toEqual({
413
+ minutes: null,
414
+ error: "Enter how many minutes before the shift.",
415
+ });
416
+ expect(validateCustomLeadMinutes(" ")).toEqual({
417
+ minutes: null,
418
+ error: "Enter how many minutes before the shift.",
419
+ });
420
+ expect(validateCustomLeadMinutes("12.5").error).toBe(
421
+ "Enter a whole number of minutes.",
422
+ );
423
+ expect(validateCustomLeadMinutes("abc").error).toBe(
424
+ "Enter a whole number of minutes.",
425
+ );
426
+ expect(
427
+ validateCustomLeadMinutes(String(MIN_MINUTES_BEFORE_SHIFT - 1)).error,
428
+ ).toBe(
429
+ "Reminders can be sent between 15 minutes and 14 days before a shift.",
430
+ );
431
+ expect(
432
+ validateCustomLeadMinutes(String(MAX_MINUTES_BEFORE_SHIFT + 1)).error,
433
+ ).toBe(
434
+ "Reminders can be sent between 15 minutes and 14 days before a shift.",
435
+ );
436
+ expect(validateCustomLeadMinutes("-15").minutes).toBeNull();
437
+ });
438
+ });
439
+
440
+ describe("upcoming shifts window and grouping", () => {
441
+ test("the window is now to now + 30 days", () => {
442
+ const now: Date = new Date("2026-08-31T12:00:00.000Z");
443
+ const window: UpcomingShiftsWindow = getUpcomingShiftsWindow(now);
444
+
445
+ expect(UPCOMING_SHIFTS_DAYS).toBe(30);
446
+ expect(window.from).toBe(now);
447
+ expect(window.to.getTime() - window.from.getTime()).toBe(
448
+ 30 * 24 * 60 * 60 * 1000,
449
+ );
450
+ });
451
+
452
+ test("the card title states the same number of days the window uses", () => {
453
+ expect(UPCOMING_SHIFTS_CARD_TITLE).toContain(String(UPCOMING_SHIFTS_DAYS));
454
+ expect(UPCOMING_SHIFTS_CARD_TITLE).toBe("Upcoming shifts (next 30 days)");
455
+ });
456
+
457
+ test("groups by the local start day, in start order, one entry per shift", () => {
458
+ const groups: Array<ShiftDayGroup> = groupShiftsByDay([
459
+ makeShift({
460
+ shiftKey: "c",
461
+ start: "2026-09-02T07:00:00.000Z",
462
+ end: "2026-09-02T15:00:00.000Z",
463
+ }),
464
+ makeShift({
465
+ shiftKey: "a",
466
+ start: "2026-09-01T07:00:00.000Z",
467
+ end: "2026-09-01T15:00:00.000Z",
468
+ }),
469
+ makeShift({
470
+ shiftKey: "b",
471
+ start: "2026-09-01T15:00:00.000Z",
472
+ end: "2026-09-01T23:00:00.000Z",
473
+ }),
474
+ ]);
475
+
476
+ expect(groups.length).toBe(2);
477
+ expect(groups[0]!.dayKey).toBe("2026-09-01");
478
+ expect(
479
+ groups[0]!.shifts.map((shift: MaterializedShiftJson): string => {
480
+ return shift.shiftKey;
481
+ }),
482
+ ).toEqual(["a", "b"]);
483
+ expect(groups[1]!.dayKey).toBe("2026-09-02");
484
+ expect(groups[1]!.shifts.length).toBe(1);
485
+ // The heading date is the first shift's start.
486
+ expect(groups[0]!.day.toISOString()).toBe("2026-09-01T07:00:00.000Z");
487
+ });
488
+
489
+ test("equal starts are ordered by shiftKey so the list is stable across renders", () => {
490
+ const groups: Array<ShiftDayGroup> = groupShiftsByDay([
491
+ makeShift({ shiftKey: "schedule-b:1" }),
492
+ makeShift({ shiftKey: "schedule-a:1" }),
493
+ ]);
494
+
495
+ expect(
496
+ groups[0]!.shifts.map((shift: MaterializedShiftJson): string => {
497
+ return shift.shiftKey;
498
+ }),
499
+ ).toEqual(["schedule-a:1", "schedule-b:1"]);
500
+ });
501
+
502
+ test("a shift across midnight is listed once, under the day it begins", () => {
503
+ const groups: Array<ShiftDayGroup> = groupShiftsByDay([
504
+ makeShift({
505
+ shiftKey: "night",
506
+ start: "2026-09-01T20:00:00.000Z",
507
+ end: "2026-09-02T08:00:00.000Z",
508
+ }),
509
+ ]);
510
+
511
+ expect(groups.length).toBe(1);
512
+ expect(groups[0]!.shifts.length).toBe(1);
513
+ });
514
+
515
+ test("an empty list groups to nothing and does not mutate its input", () => {
516
+ const input: Array<MaterializedShiftJson> = [
517
+ makeShift({ shiftKey: "z", start: "2026-09-03T07:00:00.000Z" }),
518
+ makeShift({ shiftKey: "y", start: "2026-09-01T07:00:00.000Z" }),
519
+ ];
520
+
521
+ expect(groupShiftsByDay([])).toEqual([]);
522
+ groupShiftsByDay(input);
523
+ expect(input[0]!.shiftKey).toBe("z");
524
+ });
525
+
526
+ test("isCoveringShift is true only for an override that names somebody else as the original", () => {
527
+ expect(isCoveringShift(makeShift({ shiftKey: "plain" }))).toBe(false);
528
+ expect(
529
+ isCoveringShift(
530
+ makeShift({
531
+ shiftKey: "cover",
532
+ override: {
533
+ originalUserId: "user-2",
534
+ originalUserName: "Bob",
535
+ overrideStartsAt: "2026-09-01T00:00:00.000Z",
536
+ overrideEndsAt: "2026-09-02T00:00:00.000Z",
537
+ },
538
+ }),
539
+ ),
540
+ ).toBe(true);
541
+ // A self-override (same user) is not "covering for" anybody.
542
+ expect(
543
+ isCoveringShift(
544
+ makeShift({
545
+ shiftKey: "self",
546
+ override: {
547
+ originalUserId: "user-1",
548
+ originalUserName: "Jane",
549
+ overrideStartsAt: "2026-09-01T00:00:00.000Z",
550
+ overrideEndsAt: "2026-09-02T00:00:00.000Z",
551
+ },
552
+ }),
553
+ ),
554
+ ).toBe(false);
555
+ });
556
+ });
557
+
558
+ describe("parseFeedUrls", () => {
559
+ test("returns null for a missing or non-object payload, or one without https", () => {
560
+ expect(parseFeedUrls(null)).toBeNull();
561
+ expect(parseFeedUrls(undefined)).toBeNull();
562
+ expect(parseFeedUrls("https://x")).toBeNull();
563
+ expect(parseFeedUrls({})).toBeNull();
564
+ expect(parseFeedUrls({ https: "" })).toBeNull();
565
+ expect(parseFeedUrls({ webcal: "webcals://x" })).toBeNull();
566
+ });
567
+
568
+ test("keeps every server-provided URL verbatim", () => {
569
+ expect(parseFeedUrls(BASE_URLS as unknown as JSONObject)).toEqual(
570
+ BASE_URLS,
571
+ );
572
+ });
573
+
574
+ test("repairs a missing webcal and googleAdd from the https link", () => {
575
+ const parsed: FeedUrls | null = parseFeedUrls({ https: HTTPS_URL });
576
+
577
+ expect(parsed).not.toBeNull();
578
+ /*
579
+ * webcalS, not webcal: an https feed subscribed over plain webcal:// would
580
+ * make Apple Calendar fetch the token in the clear from an https-only host
581
+ * (the server builds webcals:// for https - spec 2.2).
582
+ */
583
+ expect(parsed!.webcal).toBe(HTTPS_URL.replace("https:", "webcals:"));
584
+ expect(parsed!.webcal.startsWith("webcals://")).toBe(true);
585
+ expect(parsed!.googleAdd).toBe(buildGoogleAddUrl(HTTPS_URL));
586
+ });
587
+
588
+ test("repairs a plain-http link to webcal:// rather than webcals://", () => {
589
+ const httpUrl: string = HTTPS_URL.replace("https:", "http:");
590
+ const parsed: FeedUrls | null = parseFeedUrls({ https: httpUrl });
591
+
592
+ expect(parsed).not.toBeNull();
593
+ expect(parsed!.webcal).toBe(httpUrl.replace("http:", "webcal:"));
594
+ expect(parsed!.webcal.startsWith("webcal://")).toBe(true);
595
+ expect(parsed!.googleAdd).toBe(buildGoogleAddUrl(httpUrl));
596
+ });
597
+ });
598
+
599
+ describe("parseFeedSettings", () => {
600
+ test("falls back to the window defaults and omits the optional keys when absent", () => {
601
+ const settings: FeedSettings = parseFeedSettings({});
602
+
603
+ expect(settings).toEqual({
604
+ pastDays: DEFAULT_PAST_DAYS,
605
+ futureDays: DEFAULT_FUTURE_DAYS,
606
+ });
607
+ expect("includeCoveringShifts" in settings).toBe(false);
608
+ expect("rotateWhenMemberLeaves" in settings).toBe(false);
609
+ expect(parseFeedSettings(null)).toEqual(settings);
610
+ expect(parseFeedSettings("nope")).toEqual(settings);
611
+ });
612
+
613
+ test("reads numbers, numeric strings and the optional booleans", () => {
614
+ expect(
615
+ parseFeedSettings({
616
+ pastDays: "5",
617
+ futureDays: 120,
618
+ includeCoveringShifts: false,
619
+ includeCoverageGaps: true,
620
+ minimumGapMinutes: 45,
621
+ rotateWhenMemberLeaves: true,
622
+ }),
623
+ ).toEqual({
624
+ pastDays: 5,
625
+ futureDays: 120,
626
+ includeCoveringShifts: false,
627
+ includeCoverageGaps: true,
628
+ minimumGapMinutes: 45,
629
+ rotateWhenMemberLeaves: true,
630
+ });
631
+ });
632
+
633
+ test("ignores non-boolean and non-finite values instead of coercing them", () => {
634
+ const settings: FeedSettings = parseFeedSettings({
635
+ pastDays: "abc",
636
+ futureDays: Number.NaN,
637
+ includeCoverageGaps: "yes",
638
+ minimumGapMinutes: "60",
639
+ });
640
+
641
+ expect(settings.pastDays).toBe(DEFAULT_PAST_DAYS);
642
+ expect(settings.futureDays).toBe(DEFAULT_FUTURE_DAYS);
643
+ expect("includeCoverageGaps" in settings).toBe(false);
644
+ expect("minimumGapMinutes" in settings).toBe(false);
645
+ });
646
+ });
647
+
648
+ describe("parseFeedStatus", () => {
649
+ test("an empty or garbage payload parses to a non-existent feed rather than throwing", () => {
650
+ const empty: FeedStatus = parseFeedStatus({});
651
+
652
+ expect(empty.exists).toBe(false);
653
+ expect(empty.feedId).toBeNull();
654
+ expect(empty.isEnabled).toBe(false);
655
+ expect(empty.needsRegeneration).toBe(false);
656
+ expect(empty.tokenHint).toBeNull();
657
+ expect(empty.fetchCount).toBe(0);
658
+ expect(empty.lastRenderTruncated).toBe(false);
659
+ expect(empty.urls).toBeNull();
660
+ expect(empty.hostWarning).toBeNull();
661
+ expect(empty.protocolWarning).toBeNull();
662
+ expect(empty.settings).toEqual({
663
+ pastDays: DEFAULT_PAST_DAYS,
664
+ futureDays: DEFAULT_FUTURE_DAYS,
665
+ });
666
+
667
+ expect(parseFeedStatus(null)).toEqual(empty);
668
+ expect(parseFeedStatus(42)).toEqual(empty);
669
+ });
670
+
671
+ test("reads a full payload field for field", () => {
672
+ const status: FeedStatus = parseFeedStatus({
673
+ exists: true,
674
+ feedId: "feed-1",
675
+ isEnabled: true,
676
+ needsRegeneration: true,
677
+ tokenHint: "k3Qx",
678
+ rotatedAt: "2026-08-01T10:00:00.000Z",
679
+ previousTokenExpiresAt: "2026-08-31T10:00:00.000Z",
680
+ lastFetchedAt: "2026-08-02T10:00:00.000Z",
681
+ lastFetchedClient: "Google Calendar",
682
+ fetchCount: "143",
683
+ lastRenderTruncated: true,
684
+ settings: { pastDays: 1, futureDays: 30, includeCoveringShifts: true },
685
+ urls: BASE_URLS as unknown as JSONObject,
686
+ hostWarning: "Set HOST",
687
+ protocolWarning: "Plain http",
688
+ });
689
+
690
+ expect(status).toEqual({
691
+ exists: true,
692
+ feedId: "feed-1",
693
+ isEnabled: true,
694
+ needsRegeneration: true,
695
+ tokenHint: "k3Qx",
696
+ rotatedAt: "2026-08-01T10:00:00.000Z",
697
+ previousTokenExpiresAt: "2026-08-31T10:00:00.000Z",
698
+ lastFetchedAt: "2026-08-02T10:00:00.000Z",
699
+ lastFetchedClient: "Google Calendar",
700
+ fetchCount: 143,
701
+ lastRenderTruncated: true,
702
+ settings: { pastDays: 1, futureDays: 30, includeCoveringShifts: true },
703
+ urls: BASE_URLS,
704
+ hostWarning: "Set HOST",
705
+ protocolWarning: "Plain http",
706
+ });
707
+ });
708
+
709
+ test("booleans must be real booleans - a truthy string does not enable a feed", () => {
710
+ const status: FeedStatus = parseFeedStatus({
711
+ exists: "true",
712
+ isEnabled: 1,
713
+ needsRegeneration: "yes",
714
+ });
715
+
716
+ expect(status.exists).toBe(false);
717
+ expect(status.isEnabled).toBe(false);
718
+ expect(status.needsRegeneration).toBe(false);
719
+ });
720
+
721
+ test("empty strings read as null, not as an empty hint or warning", () => {
722
+ const status: FeedStatus = parseFeedStatus({
723
+ tokenHint: "",
724
+ hostWarning: "",
725
+ lastFetchedClient: "",
726
+ });
727
+
728
+ expect(status.tokenHint).toBeNull();
729
+ expect(status.hostWarning).toBeNull();
730
+ expect(status.lastFetchedClient).toBeNull();
731
+ });
732
+ });
733
+
734
+ describe("parseMyShifts", () => {
735
+ test("keeps only entries that carry the fields a row needs", () => {
736
+ const good: MaterializedShiftJson = makeShift({ shiftKey: "good" });
737
+ const response: MyShiftsResponse = parseMyShifts({
738
+ shifts: [
739
+ good,
740
+ { shiftKey: "no-start", scheduleId: "s", end: "x", userId: "u" },
741
+ "not an object",
742
+ null,
743
+ { ...good, shiftKey: 42 },
744
+ ],
745
+ truncated: true,
746
+ generatedAt: "2026-08-31T12:00:00.000Z",
747
+ });
748
+
749
+ expect(response.shifts).toEqual([good]);
750
+ expect(response.truncated).toBe(true);
751
+ expect(response.generatedAt).toBe("2026-08-31T12:00:00.000Z");
752
+ });
753
+
754
+ test("a payload without shifts is an empty, non-truncated list", () => {
755
+ expect(parseMyShifts({})).toEqual({
756
+ shifts: [],
757
+ truncated: false,
758
+ generatedAt: null,
759
+ });
760
+ expect(parseMyShifts(null).shifts).toEqual([]);
761
+ expect(parseMyShifts({ shifts: "nope" }).shifts).toEqual([]);
762
+ });
763
+ });
764
+
765
+ /*
766
+ * Sentence assembly. The dashboard used to build the status line by gluing
767
+ * translated fragments together in English word order ("Last fetched" + when +
768
+ * "by" + client), which no translator can reorder. Everything user-visible now
769
+ * goes through one whole-sentence key with {{placeholders}} filled here.
770
+ */
771
+ describe("interpolate", () => {
772
+ test("fills every named placeholder, repeated ones included", () => {
773
+ expect(
774
+ interpolate("Last fetched {{when}} by {{client}}", {
775
+ when: "2 hours ago",
776
+ client: "Google Calendar",
777
+ }),
778
+ ).toBe("Last fetched 2 hours ago by Google Calendar");
779
+
780
+ expect(interpolate("{{a}}-{{a}}", { a: "x" })).toBe("x-x");
781
+ });
782
+
783
+ test("accepts numbers and tolerates whitespace inside the braces", () => {
784
+ expect(interpolate("~{{count}} fetches", { count: 143 })).toBe(
785
+ "~143 fetches",
786
+ );
787
+ expect(interpolate("{{ count }} days", { count: 2 })).toBe("2 days");
788
+ expect(interpolate("{{count}} days", { count: 0 })).toBe("0 days");
789
+ });
790
+
791
+ test("leaves an unknown placeholder visible rather than blanking the sentence", () => {
792
+ expect(interpolate("Last rotated {{count}} days ago", {})).toBe(
793
+ "Last rotated {{count}} days ago",
794
+ );
795
+ });
796
+
797
+ test("a value that itself looks like a placeholder is not re-expanded", () => {
798
+ expect(interpolate("{{name}}", { name: "{{name}}" })).toBe("{{name}}");
799
+ });
800
+
801
+ test("a sentence with no placeholders is returned untouched", () => {
802
+ expect(interpolate("Not fetched yet", { count: 1 })).toBe(
803
+ "Not fetched yet",
804
+ );
805
+ });
806
+ });
807
+
808
+ describe("translateInterpolated", () => {
809
+ const GERMAN: Record<string, string> = {
810
+ "Last fetched {{when}} by {{client}}": "{{client}} hat {{when}} abgerufen",
811
+ };
812
+
813
+ const translate: (value: string | undefined) => string | undefined = (
814
+ value: string | undefined,
815
+ ): string | undefined => {
816
+ return GERMAN[value || ""] || value;
817
+ };
818
+
819
+ test("a translation may reorder the placeholders - the whole point of the change", () => {
820
+ expect(
821
+ translateInterpolated(translate, "Last fetched {{when}} by {{client}}", {
822
+ when: "vor 2 Stunden",
823
+ client: "Google Calendar",
824
+ }),
825
+ ).toBe("Google Calendar hat vor 2 Stunden abgerufen");
826
+ });
827
+
828
+ test("falls back to the English sentence when the locale has no entry", () => {
829
+ expect(
830
+ translateInterpolated(translate, "Last fetched {{when}}", {
831
+ when: "2 hours ago",
832
+ }),
833
+ ).toBe("Last fetched 2 hours ago");
834
+ });
835
+
836
+ test("an empty translation is treated as missing, never rendered as a blank line", () => {
837
+ expect(
838
+ translateInterpolated(
839
+ (): string => {
840
+ return "";
841
+ },
842
+ "Covering for {{name}}",
843
+ { name: "Bob" },
844
+ ),
845
+ ).toBe("Covering for Bob");
846
+ });
847
+
848
+ test("works with no translator at all, which is what keeps the helper pure", () => {
849
+ expect(
850
+ translateInterpolated(undefined, "Only on policy {{name}}", {
851
+ name: "Checkout",
852
+ }),
853
+ ).toBe("Only on policy Checkout");
854
+ });
855
+ });