@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.
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/Index.ts +11 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Date.ts +143 -13
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/Types/Rum/SessionReplay.ts +62 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
cleanup,
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
screen,
|
|
15
|
+
waitFor,
|
|
16
|
+
within,
|
|
17
|
+
} from "@testing-library/react";
|
|
18
|
+
import React from "react";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* The reader's own calendar link, rendered for real against a stubbed API.
|
|
23
|
+
*
|
|
24
|
+
* What this file guards: the link is only ever minted through POST /rotate
|
|
25
|
+
* (there is no client-side token anywhere), the four states a feed can be in
|
|
26
|
+
* (absent / active / disabled / unreadable) each look different and offer the
|
|
27
|
+
* right next step, every write goes to the right route or model with the
|
|
28
|
+
* right payload, an older API (404) hides the feature rather than erroring,
|
|
29
|
+
* and the schedule variant narrows the same link with ?schedule= while
|
|
30
|
+
* pointing back to the full page for everything else.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const getMock: MockFunction = getJestMockFunction();
|
|
34
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
35
|
+
const updateByIdMock: MockFunction = getJestMockFunction();
|
|
36
|
+
const deleteItemMock: MockFunction = getJestMockFunction();
|
|
37
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
38
|
+
|
|
39
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
40
|
+
return {
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: {
|
|
43
|
+
get: (...args: Array<any>) => {
|
|
44
|
+
return getMock(...args);
|
|
45
|
+
},
|
|
46
|
+
post: (...args: Array<any>) => {
|
|
47
|
+
return postMock(...args);
|
|
48
|
+
},
|
|
49
|
+
getFriendlyMessage: (error: unknown): string => {
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
return error.message;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
error &&
|
|
56
|
+
typeof error === "object" &&
|
|
57
|
+
"message" in (error as Record<string, unknown>)
|
|
58
|
+
) {
|
|
59
|
+
return String((error as Record<string, unknown>)["message"]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return "Something went wrong";
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
69
|
+
return {
|
|
70
|
+
__esModule: true,
|
|
71
|
+
default: {
|
|
72
|
+
getCommonHeaders: (): Record<string, string> => {
|
|
73
|
+
return {};
|
|
74
|
+
},
|
|
75
|
+
updateById: (...args: Array<any>) => {
|
|
76
|
+
return updateByIdMock(...args);
|
|
77
|
+
},
|
|
78
|
+
deleteItem: (...args: Array<any>) => {
|
|
79
|
+
return deleteItemMock(...args);
|
|
80
|
+
},
|
|
81
|
+
getList: (...args: Array<any>) => {
|
|
82
|
+
return getListMock(...args);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The settings card is ModelDetail machinery (its own fetch, its own form).
|
|
90
|
+
* It is replaced with a stub that prints what it was asked to show, so the
|
|
91
|
+
* assertions here are about WHICH model, id and fields the card is wired to,
|
|
92
|
+
* not about ModelDetail itself.
|
|
93
|
+
*/
|
|
94
|
+
jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
|
|
95
|
+
return {
|
|
96
|
+
__esModule: true,
|
|
97
|
+
default: (props: Record<string, any>) => {
|
|
98
|
+
const fieldTitles: Array<string> = (props["formFields"] || []).map(
|
|
99
|
+
(field: Record<string, unknown>): string => {
|
|
100
|
+
return String(field["title"]);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return React.createElement(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
"data-testid": "card-model-detail-stub",
|
|
108
|
+
"data-model-id": String(props["modelDetailProps"]?.modelId),
|
|
109
|
+
"data-model-name": String(
|
|
110
|
+
new props["modelDetailProps"].modelType().tableName,
|
|
111
|
+
),
|
|
112
|
+
"data-editable": String(props["isEditable"]),
|
|
113
|
+
},
|
|
114
|
+
`${props["cardProps"]?.title}|${fieldTitles.join(",")}`,
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
jest.mock("../../../UI/Utils/User", () => {
|
|
121
|
+
return {
|
|
122
|
+
__esModule: true,
|
|
123
|
+
default: {
|
|
124
|
+
getUserId: (): ObjectID => {
|
|
125
|
+
return new ObjectID("44444444-4444-4444-8444-444444444444");
|
|
126
|
+
},
|
|
127
|
+
isMasterAdmin: (): boolean => {
|
|
128
|
+
return false;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
jest.mock("react-i18next", () => {
|
|
135
|
+
return {
|
|
136
|
+
useTranslation: () => {
|
|
137
|
+
return {
|
|
138
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
139
|
+
return options?.defaultValue ?? key;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
147
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
148
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
149
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
150
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
151
|
+
import UserOnCallCalendarFeed from "../../../Models/DatabaseModels/UserOnCallCalendarFeed";
|
|
152
|
+
import FeedStatusLine from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/FeedStatusLine";
|
|
153
|
+
import { FeedStatus } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedTypes";
|
|
154
|
+
import {
|
|
155
|
+
PERSONAL_FEED_CURRENT_PATH,
|
|
156
|
+
PERSONAL_FEED_ROTATE_PATH,
|
|
157
|
+
PLANNING_NOT_AUDIT_COPY,
|
|
158
|
+
buildGoogleAddUrl,
|
|
159
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedUtil";
|
|
160
|
+
import PersonalCalendarFeedCard, {
|
|
161
|
+
PersonalCalendarFeedVariant,
|
|
162
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/PersonalCalendarFeedCard";
|
|
163
|
+
import UserSettingsOnCallCalendarFeed from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/OnCallCalendarFeed";
|
|
164
|
+
import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
|
|
165
|
+
import RouteMap, {
|
|
166
|
+
RouteUtil,
|
|
167
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
|
|
168
|
+
import Route from "../../../Types/API/Route";
|
|
169
|
+
|
|
170
|
+
const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
|
|
171
|
+
const FEED_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
172
|
+
const SCHEDULE_ID: ObjectID = new ObjectID(
|
|
173
|
+
"33333333-3333-4333-8333-333333333333",
|
|
174
|
+
);
|
|
175
|
+
const NOW: Date = new Date("2026-08-31T12:00:00.000Z");
|
|
176
|
+
|
|
177
|
+
const HTTPS_URL: string = `https://oneuptime.example.com/api/on-call-calendar/user/abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG/shifts.ics`;
|
|
178
|
+
const WEBCAL_URL: string = HTTPS_URL.replace("https:", "webcals:");
|
|
179
|
+
|
|
180
|
+
type StatusJsonFunction = (overrides?: JSONObject) => JSONObject;
|
|
181
|
+
|
|
182
|
+
const activeStatusJson: StatusJsonFunction = (
|
|
183
|
+
overrides?: JSONObject,
|
|
184
|
+
): JSONObject => {
|
|
185
|
+
return {
|
|
186
|
+
exists: true,
|
|
187
|
+
feedId: FEED_ID,
|
|
188
|
+
isEnabled: true,
|
|
189
|
+
needsRegeneration: false,
|
|
190
|
+
tokenHint: "k3Qx",
|
|
191
|
+
rotatedAt: "2026-08-01T10:00:00.000Z",
|
|
192
|
+
previousTokenExpiresAt: null,
|
|
193
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
194
|
+
lastFetchedClient: "Google Calendar",
|
|
195
|
+
fetchCount: 143,
|
|
196
|
+
lastRenderTruncated: false,
|
|
197
|
+
settings: {
|
|
198
|
+
includeCoveringShifts: true,
|
|
199
|
+
pastDays: 2,
|
|
200
|
+
futureDays: 90,
|
|
201
|
+
},
|
|
202
|
+
urls: {
|
|
203
|
+
https: HTTPS_URL,
|
|
204
|
+
webcal: WEBCAL_URL,
|
|
205
|
+
googleAdd: buildGoogleAddUrl(HTTPS_URL),
|
|
206
|
+
},
|
|
207
|
+
hostWarning: null,
|
|
208
|
+
protocolWarning: null,
|
|
209
|
+
...overrides,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const EMPTY_STATUS_JSON: JSONObject = {
|
|
214
|
+
exists: false,
|
|
215
|
+
feedId: null,
|
|
216
|
+
isEnabled: false,
|
|
217
|
+
needsRegeneration: false,
|
|
218
|
+
tokenHint: null,
|
|
219
|
+
rotatedAt: null,
|
|
220
|
+
previousTokenExpiresAt: null,
|
|
221
|
+
lastFetchedAt: null,
|
|
222
|
+
lastFetchedClient: null,
|
|
223
|
+
fetchCount: 0,
|
|
224
|
+
lastRenderTruncated: false,
|
|
225
|
+
settings: { pastDays: 2, futureDays: 90 },
|
|
226
|
+
urls: null,
|
|
227
|
+
hostWarning: null,
|
|
228
|
+
protocolWarning: null,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
type OkFunction = (json: JSONObject) => HTTPResponse<JSONObject>;
|
|
232
|
+
|
|
233
|
+
const ok: OkFunction = (json: JSONObject): HTTPResponse<JSONObject> => {
|
|
234
|
+
return new HTTPResponse<JSONObject>(200, json, {});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
type FailFunction = (status: number, message: string) => HTTPErrorResponse;
|
|
238
|
+
|
|
239
|
+
const fail: FailFunction = (
|
|
240
|
+
status: number,
|
|
241
|
+
message: string,
|
|
242
|
+
): HTTPErrorResponse => {
|
|
243
|
+
return new HTTPErrorResponse(status, { message: message }, {});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
type UrlOfFunction = (call: Array<unknown>) => string;
|
|
247
|
+
|
|
248
|
+
const urlOf: UrlOfFunction = (call: Array<unknown>): string => {
|
|
249
|
+
return String((call[0] as Record<string, unknown>)["url"]);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type StatusFunction = (overrides?: Partial<FeedStatus>) => FeedStatus;
|
|
253
|
+
|
|
254
|
+
const status: StatusFunction = (
|
|
255
|
+
overrides?: Partial<FeedStatus>,
|
|
256
|
+
): FeedStatus => {
|
|
257
|
+
return {
|
|
258
|
+
exists: true,
|
|
259
|
+
feedId: FEED_ID,
|
|
260
|
+
isEnabled: true,
|
|
261
|
+
needsRegeneration: false,
|
|
262
|
+
tokenHint: "k3Qx",
|
|
263
|
+
rotatedAt: "2026-08-01T10:00:00.000Z",
|
|
264
|
+
previousTokenExpiresAt: null,
|
|
265
|
+
lastFetchedAt: null,
|
|
266
|
+
lastFetchedClient: null,
|
|
267
|
+
fetchCount: 0,
|
|
268
|
+
lastRenderTruncated: false,
|
|
269
|
+
settings: { pastDays: 2, futureDays: 90 },
|
|
270
|
+
urls: null,
|
|
271
|
+
hostWarning: null,
|
|
272
|
+
protocolWarning: null,
|
|
273
|
+
...overrides,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
function goToCalendarFeedPage(): void {
|
|
278
|
+
window.history.pushState(
|
|
279
|
+
{},
|
|
280
|
+
"",
|
|
281
|
+
`/dashboard/${PROJECT_ID}/user-settings/calendar-feed`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resetMocks(): void {
|
|
286
|
+
getMock.mockReset();
|
|
287
|
+
postMock.mockReset();
|
|
288
|
+
updateByIdMock.mockReset();
|
|
289
|
+
deleteItemMock.mockReset();
|
|
290
|
+
getListMock.mockReset();
|
|
291
|
+
updateByIdMock.mockResolvedValue(undefined);
|
|
292
|
+
deleteItemMock.mockResolvedValue(undefined);
|
|
293
|
+
getListMock.mockResolvedValue({ data: [], count: 0, skip: 0, limit: 10 });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
describe("FeedStatusLine", () => {
|
|
297
|
+
afterEach(() => {
|
|
298
|
+
cleanup();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("says 'Not fetched yet' and the link hint when nothing has fetched the link", () => {
|
|
302
|
+
render(<FeedStatusLine status={status()} now={NOW} />);
|
|
303
|
+
|
|
304
|
+
const line: HTMLElement = screen.getByTestId("calendar-feed-status-line");
|
|
305
|
+
|
|
306
|
+
expect(line).toHaveTextContent("Not fetched yet");
|
|
307
|
+
expect(line).toHaveTextContent("link ending in …k3Qx");
|
|
308
|
+
expect(line).not.toHaveTextContent("Last rotated");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("composes last fetched / client / approximate count / hint", () => {
|
|
312
|
+
render(
|
|
313
|
+
<FeedStatusLine
|
|
314
|
+
status={status({
|
|
315
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
316
|
+
lastFetchedClient: "Google Calendar",
|
|
317
|
+
fetchCount: 143,
|
|
318
|
+
})}
|
|
319
|
+
now={NOW}
|
|
320
|
+
/>,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const line: HTMLElement = screen.getByTestId("calendar-feed-status-line");
|
|
324
|
+
|
|
325
|
+
expect(line).toHaveTextContent("Last fetched");
|
|
326
|
+
expect(line).toHaveTextContent("by Google Calendar");
|
|
327
|
+
expect(line).toHaveTextContent("~143 fetches");
|
|
328
|
+
expect(line).toHaveTextContent("link ending in …k3Qx");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("uses the singular for exactly one fetch and omits the count at zero", () => {
|
|
332
|
+
const { rerender } = render(
|
|
333
|
+
<FeedStatusLine
|
|
334
|
+
status={status({
|
|
335
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
336
|
+
fetchCount: 1,
|
|
337
|
+
})}
|
|
338
|
+
now={NOW}
|
|
339
|
+
/>,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(screen.getByTestId("calendar-feed-status-line")).toHaveTextContent(
|
|
343
|
+
"~1 fetch",
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
rerender(
|
|
347
|
+
<FeedStatusLine
|
|
348
|
+
status={status({
|
|
349
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
350
|
+
fetchCount: 0,
|
|
351
|
+
})}
|
|
352
|
+
now={NOW}
|
|
353
|
+
/>,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
screen.getByTestId("calendar-feed-status-line"),
|
|
358
|
+
).not.toHaveTextContent("~");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("shows the rotation age only when asked, with today / 1 day / N days phrasing", () => {
|
|
362
|
+
const { rerender } = render(
|
|
363
|
+
<FeedStatusLine
|
|
364
|
+
status={status({ rotatedAt: "2026-08-01T10:00:00.000Z" })}
|
|
365
|
+
now={NOW}
|
|
366
|
+
showRotatedAgo={true}
|
|
367
|
+
/>,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(screen.getByTestId("calendar-feed-status-line")).toHaveTextContent(
|
|
371
|
+
"Last rotated 30 days ago",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
rerender(
|
|
375
|
+
<FeedStatusLine
|
|
376
|
+
status={status({ rotatedAt: "2026-08-30T08:00:00.000Z" })}
|
|
377
|
+
now={NOW}
|
|
378
|
+
showRotatedAgo={true}
|
|
379
|
+
/>,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(screen.getByTestId("calendar-feed-status-line")).toHaveTextContent(
|
|
383
|
+
"Last rotated 1 day ago",
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
rerender(
|
|
387
|
+
<FeedStatusLine
|
|
388
|
+
status={status({ rotatedAt: "2026-08-31T08:00:00.000Z" })}
|
|
389
|
+
now={NOW}
|
|
390
|
+
showRotatedAgo={true}
|
|
391
|
+
/>,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(screen.getByTestId("calendar-feed-status-line")).toHaveTextContent(
|
|
395
|
+
"Last rotated today",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("the reachability hint appears only after 48 hours without a fetch", () => {
|
|
400
|
+
const { rerender } = render(
|
|
401
|
+
<FeedStatusLine
|
|
402
|
+
status={status({ rotatedAt: "2026-08-01T10:00:00.000Z" })}
|
|
403
|
+
now={NOW}
|
|
404
|
+
/>,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(
|
|
408
|
+
screen.getByTestId("calendar-feed-nothing-fetched-hint"),
|
|
409
|
+
).toHaveTextContent("Nothing has fetched this link yet");
|
|
410
|
+
|
|
411
|
+
rerender(
|
|
412
|
+
<FeedStatusLine
|
|
413
|
+
status={status({ rotatedAt: "2026-08-31T00:00:00.000Z" })}
|
|
414
|
+
now={NOW}
|
|
415
|
+
/>,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(
|
|
419
|
+
screen.queryByTestId("calendar-feed-nothing-fetched-hint"),
|
|
420
|
+
).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("idPrefix namespaces the test ids", () => {
|
|
424
|
+
render(<FeedStatusLine status={status()} now={NOW} idPrefix="shared" />);
|
|
425
|
+
|
|
426
|
+
expect(screen.getByTestId("shared-status-line")).toBeInTheDocument();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/*
|
|
430
|
+
* Every segment is one whole translated sentence with {{placeholders}}
|
|
431
|
+
* filled in by the component, not a translated word glued to a value. If a
|
|
432
|
+
* placeholder ever reached the DOM it would mean the sentence was looked up
|
|
433
|
+
* but never interpolated - a visible break in all 16 locales.
|
|
434
|
+
*/
|
|
435
|
+
test("no interpolation placeholder ever reaches the rendered line", () => {
|
|
436
|
+
render(
|
|
437
|
+
<FeedStatusLine
|
|
438
|
+
status={status({
|
|
439
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
440
|
+
lastFetchedClient: "Google Calendar",
|
|
441
|
+
fetchCount: 143,
|
|
442
|
+
rotatedAt: "2026-08-01T10:00:00.000Z",
|
|
443
|
+
})}
|
|
444
|
+
now={NOW}
|
|
445
|
+
showRotatedAgo={true}
|
|
446
|
+
/>,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const line: HTMLElement = screen.getByTestId("calendar-feed-status-line");
|
|
450
|
+
|
|
451
|
+
expect(line.textContent).not.toContain("{{");
|
|
452
|
+
expect(line.textContent).not.toContain("}}");
|
|
453
|
+
// Every value still lands in the sentence it belongs to.
|
|
454
|
+
expect(line).toHaveTextContent("by Google Calendar");
|
|
455
|
+
expect(line).toHaveTextContent("~143 fetches");
|
|
456
|
+
expect(line).toHaveTextContent("link ending in …k3Qx");
|
|
457
|
+
expect(line).toHaveTextContent("Last rotated 30 days ago");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("the client is left out of the sentence rather than trailing an empty 'by'", () => {
|
|
461
|
+
render(
|
|
462
|
+
<FeedStatusLine
|
|
463
|
+
status={status({
|
|
464
|
+
lastFetchedAt: "2026-08-31T10:00:00.000Z",
|
|
465
|
+
lastFetchedClient: null,
|
|
466
|
+
fetchCount: 4,
|
|
467
|
+
})}
|
|
468
|
+
now={NOW}
|
|
469
|
+
/>,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const line: HTMLElement = screen.getByTestId("calendar-feed-status-line");
|
|
473
|
+
|
|
474
|
+
expect(line).toHaveTextContent("Last fetched");
|
|
475
|
+
expect(line.textContent).not.toContain(" by ");
|
|
476
|
+
expect(line.textContent).not.toContain("{{");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("PersonalCalendarFeedCard (full variant)", () => {
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
resetMocks();
|
|
483
|
+
goToCalendarFeedPage();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
afterEach(() => {
|
|
487
|
+
cleanup();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("reads /feed/current on mount and shows the empty state with a Generate button", async () => {
|
|
491
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
492
|
+
|
|
493
|
+
render(
|
|
494
|
+
<PersonalCalendarFeedCard
|
|
495
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
496
|
+
now={NOW}
|
|
497
|
+
/>,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
await waitFor(() => {
|
|
501
|
+
expect(
|
|
502
|
+
screen.getByTestId("personal-calendar-feed-empty"),
|
|
503
|
+
).toBeInTheDocument();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
507
|
+
expect(urlOf(getMock.mock.calls[0] as Array<unknown>)).toContain(
|
|
508
|
+
PERSONAL_FEED_CURRENT_PATH,
|
|
509
|
+
);
|
|
510
|
+
expect(
|
|
511
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
512
|
+
).toBeInTheDocument();
|
|
513
|
+
// No card buttons (regenerate / disable / delete) before a link exists.
|
|
514
|
+
expect(screen.queryByTestId("card-button")).not.toBeInTheDocument();
|
|
515
|
+
// No settings card before a link exists.
|
|
516
|
+
expect(
|
|
517
|
+
screen.queryByTestId("card-model-detail-stub"),
|
|
518
|
+
).not.toBeInTheDocument();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
/*
|
|
522
|
+
* The server sends hostWarning / protocolWarning even when no feed exists.
|
|
523
|
+
* Saying "this link will be unreachable" only AFTER the reader has minted
|
|
524
|
+
* one and pasted it into Google Calendar wastes the one moment the warning
|
|
525
|
+
* is actionable.
|
|
526
|
+
*/
|
|
527
|
+
test("the empty state shows the deployment warnings the server attached to it", async () => {
|
|
528
|
+
getMock.mockResolvedValue(
|
|
529
|
+
ok({
|
|
530
|
+
...EMPTY_STATUS_JSON,
|
|
531
|
+
hostWarning: "HOST is not set, so the link points at localhost.",
|
|
532
|
+
protocolWarning: "HTTP_PROTOCOL is http, so the link is not encrypted.",
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
render(
|
|
537
|
+
<PersonalCalendarFeedCard
|
|
538
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
539
|
+
now={NOW}
|
|
540
|
+
/>,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(
|
|
545
|
+
screen.getByTestId("personal-calendar-feed-empty"),
|
|
546
|
+
).toBeInTheDocument();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(
|
|
550
|
+
screen.getByTestId("personal-calendar-feed-host-warning"),
|
|
551
|
+
).toHaveTextContent("HOST is not set");
|
|
552
|
+
expect(
|
|
553
|
+
screen.getByTestId("personal-calendar-feed-protocol-warning"),
|
|
554
|
+
).toHaveTextContent("HTTP_PROTOCOL is http");
|
|
555
|
+
// Still offered - the warning informs the click, it does not block it.
|
|
556
|
+
expect(
|
|
557
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
558
|
+
).toBeInTheDocument();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("Generate posts to /feed/rotate with an empty JSON body and renders the returned link", async () => {
|
|
562
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
563
|
+
postMock.mockResolvedValue(ok(activeStatusJson()));
|
|
564
|
+
|
|
565
|
+
render(
|
|
566
|
+
<PersonalCalendarFeedCard
|
|
567
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
568
|
+
now={NOW}
|
|
569
|
+
/>,
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
await waitFor(() => {
|
|
573
|
+
expect(
|
|
574
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
575
|
+
).toBeInTheDocument();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
fireEvent.click(screen.getByTestId("personal-calendar-feed-generate"));
|
|
579
|
+
|
|
580
|
+
await waitFor(() => {
|
|
581
|
+
expect(
|
|
582
|
+
screen.getByTestId("personal-calendar-feed-active"),
|
|
583
|
+
).toBeInTheDocument();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
expect(postMock).toHaveBeenCalledTimes(1);
|
|
587
|
+
expect(urlOf(postMock.mock.calls[0] as Array<unknown>)).toContain(
|
|
588
|
+
PERSONAL_FEED_ROTATE_PATH,
|
|
589
|
+
);
|
|
590
|
+
expect(
|
|
591
|
+
(postMock.mock.calls[0]![0] as Record<string, unknown>)["data"],
|
|
592
|
+
).toEqual({});
|
|
593
|
+
|
|
594
|
+
// The link block, hidden until revealed, and the webcal anchor.
|
|
595
|
+
expect(
|
|
596
|
+
screen.getByTestId("personal-calendar-feed-links"),
|
|
597
|
+
).toBeInTheDocument();
|
|
598
|
+
expect(screen.getByTestId("personal-calendar-feed-webcal")).toHaveAttribute(
|
|
599
|
+
"href",
|
|
600
|
+
WEBCAL_URL,
|
|
601
|
+
);
|
|
602
|
+
expect(screen.queryByText(HTTPS_URL)).not.toBeInTheDocument();
|
|
603
|
+
|
|
604
|
+
// The settings card is wired to the personal feed model and this feed id.
|
|
605
|
+
const settings: HTMLElement = screen.getByTestId("card-model-detail-stub");
|
|
606
|
+
expect(settings).toHaveAttribute("data-model-id", FEED_ID);
|
|
607
|
+
expect(settings).toHaveAttribute(
|
|
608
|
+
"data-model-name",
|
|
609
|
+
new UserOnCallCalendarFeed().tableName,
|
|
610
|
+
);
|
|
611
|
+
expect(settings).toHaveTextContent(
|
|
612
|
+
"Calendar feed settings|Include shifts I cover for others,Days of past shifts,Days ahead",
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("an active feed shows the bookkeeping line, the Time Log pointer and the refresh alert", async () => {
|
|
617
|
+
getMock.mockResolvedValue(ok(activeStatusJson()));
|
|
618
|
+
|
|
619
|
+
render(
|
|
620
|
+
<PersonalCalendarFeedCard
|
|
621
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
622
|
+
now={NOW}
|
|
623
|
+
/>,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await waitFor(() => {
|
|
627
|
+
expect(
|
|
628
|
+
screen.getByTestId("personal-calendar-feed-active"),
|
|
629
|
+
).toBeInTheDocument();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(
|
|
633
|
+
screen.getByTestId("personal-calendar-feed-status-line"),
|
|
634
|
+
).toHaveTextContent("by Google Calendar");
|
|
635
|
+
expect(
|
|
636
|
+
screen.getByTestId("personal-calendar-feed-status-line"),
|
|
637
|
+
).toHaveTextContent("~143 fetches");
|
|
638
|
+
|
|
639
|
+
const timeLog: HTMLElement = screen.getByTestId(
|
|
640
|
+
"personal-calendar-feed-time-log",
|
|
641
|
+
);
|
|
642
|
+
expect(timeLog).toHaveTextContent(PLANNING_NOT_AUDIT_COPY);
|
|
643
|
+
const expectedTimeLogRoute: Route = RouteUtil.populateRouteParams(
|
|
644
|
+
RouteMap[PageMap.ON_CALLDUTY_USER_TIME_LOGS] as Route,
|
|
645
|
+
);
|
|
646
|
+
expect(
|
|
647
|
+
within(timeLog).getByText("Open the On-Call Time Log").closest("a"),
|
|
648
|
+
).toHaveAttribute("href", expectedTimeLogRoute.toString());
|
|
649
|
+
|
|
650
|
+
expect(
|
|
651
|
+
screen.getByTestId("personal-calendar-feed-refresh-alert"),
|
|
652
|
+
).toBeInTheDocument();
|
|
653
|
+
|
|
654
|
+
// Regenerate / Disable / Delete are offered once a link exists.
|
|
655
|
+
expect(
|
|
656
|
+
screen.getByRole("button", { name: "Regenerate link" }),
|
|
657
|
+
).toBeInTheDocument();
|
|
658
|
+
expect(screen.getByRole("button", { name: "Disable" })).toBeInTheDocument();
|
|
659
|
+
expect(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
/*
|
|
663
|
+
* What the server does with the rotated-out token inside its grace window is
|
|
664
|
+
* answer with an EMPTY calendar (spec 2.1), not with the old shifts. The
|
|
665
|
+
* line has to say that: "keeps working until <date>" would tell somebody who
|
|
666
|
+
* rotated because a colleague left that the leaver still sees the roster for
|
|
667
|
+
* a month.
|
|
668
|
+
*/
|
|
669
|
+
test("a rotated-out previous link is announced as empty until its expiry, not as still working", async () => {
|
|
670
|
+
getMock.mockResolvedValue(
|
|
671
|
+
ok(
|
|
672
|
+
activeStatusJson({
|
|
673
|
+
previousTokenExpiresAt: "2026-09-30T10:00:00.000Z",
|
|
674
|
+
}),
|
|
675
|
+
),
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
render(
|
|
679
|
+
<PersonalCalendarFeedCard
|
|
680
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
681
|
+
now={NOW}
|
|
682
|
+
/>,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
let line: HTMLElement | null = null;
|
|
686
|
+
|
|
687
|
+
await waitFor(() => {
|
|
688
|
+
line = screen.getByTestId("personal-calendar-feed-previous-link");
|
|
689
|
+
expect(line).toHaveTextContent(
|
|
690
|
+
"Your previous link returns an empty calendar until",
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
expect(line!).toHaveTextContent("then stops working");
|
|
695
|
+
expect(line!).not.toHaveTextContent("keeps working");
|
|
696
|
+
// The {{date}} placeholder is filled, not printed.
|
|
697
|
+
expect(line!.textContent).not.toContain("{{");
|
|
698
|
+
expect(line!.textContent).toContain(
|
|
699
|
+
OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
700
|
+
OneUptimeDate.fromString("2026-09-30T10:00:00.000Z"),
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("Regenerate asks for confirmation, then posts to /feed/rotate", async () => {
|
|
706
|
+
getMock.mockResolvedValue(ok(activeStatusJson()));
|
|
707
|
+
postMock.mockResolvedValue(ok(activeStatusJson({ tokenHint: "n3wX" })));
|
|
708
|
+
|
|
709
|
+
render(
|
|
710
|
+
<PersonalCalendarFeedCard
|
|
711
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
712
|
+
now={NOW}
|
|
713
|
+
/>,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
await waitFor(() => {
|
|
717
|
+
expect(
|
|
718
|
+
screen.getByRole("button", { name: "Regenerate link" }),
|
|
719
|
+
).toBeInTheDocument();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
fireEvent.click(screen.getByRole("button", { name: "Regenerate link" }));
|
|
723
|
+
|
|
724
|
+
const modal: HTMLElement = await screen.findByTestId("modal");
|
|
725
|
+
/*
|
|
726
|
+
* The modal must not promise the old link keeps serving shifts: the server
|
|
727
|
+
* answers it with an empty calendar for the 30-day grace, then 404s.
|
|
728
|
+
*/
|
|
729
|
+
const description: HTMLElement = within(modal).getByTestId(
|
|
730
|
+
"confirm-modal-description",
|
|
731
|
+
);
|
|
732
|
+
expect(description).toHaveTextContent(
|
|
733
|
+
"For 30 days the old link keeps answering with that empty calendar instead of an error, then it stops working",
|
|
734
|
+
);
|
|
735
|
+
expect(description).not.toHaveTextContent("keeps working for 30 days");
|
|
736
|
+
// Nothing is posted until the reader confirms.
|
|
737
|
+
expect(postMock).not.toHaveBeenCalled();
|
|
738
|
+
|
|
739
|
+
fireEvent.click(
|
|
740
|
+
within(modal).getByRole("button", { name: "Regenerate link" }),
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
await waitFor(() => {
|
|
744
|
+
expect(postMock).toHaveBeenCalledTimes(1);
|
|
745
|
+
});
|
|
746
|
+
expect(urlOf(postMock.mock.calls[0] as Array<unknown>)).toContain(
|
|
747
|
+
PERSONAL_FEED_ROTATE_PATH,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
await waitFor(() => {
|
|
751
|
+
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
|
752
|
+
});
|
|
753
|
+
expect(
|
|
754
|
+
screen.getByTestId("personal-calendar-feed-status-line"),
|
|
755
|
+
).toHaveTextContent("…n3wX");
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("Disable updates isEnabled on the personal feed row and re-reads the status", async () => {
|
|
759
|
+
getMock
|
|
760
|
+
.mockResolvedValueOnce(ok(activeStatusJson()))
|
|
761
|
+
.mockResolvedValueOnce(ok(activeStatusJson({ isEnabled: false })));
|
|
762
|
+
|
|
763
|
+
render(
|
|
764
|
+
<PersonalCalendarFeedCard
|
|
765
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
766
|
+
now={NOW}
|
|
767
|
+
/>,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
await waitFor(() => {
|
|
771
|
+
expect(
|
|
772
|
+
screen.getByRole("button", { name: "Disable" }),
|
|
773
|
+
).toBeInTheDocument();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
fireEvent.click(screen.getByRole("button", { name: "Disable" }));
|
|
777
|
+
|
|
778
|
+
await waitFor(() => {
|
|
779
|
+
expect(updateByIdMock).toHaveBeenCalledTimes(1);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const call: Record<string, unknown> = updateByIdMock.mock
|
|
783
|
+
.calls[0]![0] as Record<string, unknown>;
|
|
784
|
+
expect(call["modelType"]).toBe(UserOnCallCalendarFeed);
|
|
785
|
+
expect(String(call["id"])).toBe(FEED_ID);
|
|
786
|
+
expect(call["data"]).toEqual({ isEnabled: false });
|
|
787
|
+
|
|
788
|
+
await waitFor(() => {
|
|
789
|
+
expect(
|
|
790
|
+
screen.getByTestId("personal-calendar-feed-disabled"),
|
|
791
|
+
).toBeInTheDocument();
|
|
792
|
+
});
|
|
793
|
+
expect(getMock).toHaveBeenCalledTimes(2);
|
|
794
|
+
/*
|
|
795
|
+
* A disabled feed offers Enable; the link stays visible under the
|
|
796
|
+
* warning so the reader can still copy it for later.
|
|
797
|
+
*/
|
|
798
|
+
expect(screen.getByRole("button", { name: "Enable" })).toBeInTheDocument();
|
|
799
|
+
expect(
|
|
800
|
+
screen.getByTestId("personal-calendar-feed-links"),
|
|
801
|
+
).toBeInTheDocument();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("Delete asks for confirmation, then deletes the row and re-reads", async () => {
|
|
805
|
+
getMock
|
|
806
|
+
.mockResolvedValueOnce(ok(activeStatusJson()))
|
|
807
|
+
.mockResolvedValueOnce(ok(EMPTY_STATUS_JSON));
|
|
808
|
+
|
|
809
|
+
render(
|
|
810
|
+
<PersonalCalendarFeedCard
|
|
811
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
812
|
+
now={NOW}
|
|
813
|
+
/>,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
await waitFor(() => {
|
|
817
|
+
expect(
|
|
818
|
+
screen.getByRole("button", { name: "Delete" }),
|
|
819
|
+
).toBeInTheDocument();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
fireEvent.click(screen.getByRole("button", { name: "Delete" }));
|
|
823
|
+
|
|
824
|
+
const modal: HTMLElement = await screen.findByTestId("modal");
|
|
825
|
+
expect(deleteItemMock).not.toHaveBeenCalled();
|
|
826
|
+
|
|
827
|
+
fireEvent.click(within(modal).getByRole("button", { name: "Delete" }));
|
|
828
|
+
|
|
829
|
+
await waitFor(() => {
|
|
830
|
+
expect(deleteItemMock).toHaveBeenCalledTimes(1);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const call: Record<string, unknown> = deleteItemMock.mock
|
|
834
|
+
.calls[0]![0] as Record<string, unknown>;
|
|
835
|
+
expect(call["modelType"]).toBe(UserOnCallCalendarFeed);
|
|
836
|
+
expect(String(call["id"])).toBe(FEED_ID);
|
|
837
|
+
|
|
838
|
+
await waitFor(() => {
|
|
839
|
+
expect(
|
|
840
|
+
screen.getByTestId("personal-calendar-feed-empty"),
|
|
841
|
+
).toBeInTheDocument();
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("an unreadable stored link shows the regenerate warning and no link", async () => {
|
|
846
|
+
getMock.mockResolvedValue(
|
|
847
|
+
ok(activeStatusJson({ needsRegeneration: true, urls: null })),
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
render(
|
|
851
|
+
<PersonalCalendarFeedCard
|
|
852
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
853
|
+
now={NOW}
|
|
854
|
+
/>,
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
await waitFor(() => {
|
|
858
|
+
expect(
|
|
859
|
+
screen.getByTestId("personal-calendar-feed-needs-regeneration"),
|
|
860
|
+
).toHaveTextContent("encryption secret changed");
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
expect(
|
|
864
|
+
screen.queryByTestId("personal-calendar-feed-links"),
|
|
865
|
+
).not.toBeInTheDocument();
|
|
866
|
+
expect(
|
|
867
|
+
screen.getByRole("button", { name: "Regenerate link" }),
|
|
868
|
+
).toBeInTheDocument();
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("server warnings attached to the link are shown next to it", async () => {
|
|
872
|
+
getMock.mockResolvedValue(
|
|
873
|
+
ok(
|
|
874
|
+
activeStatusJson({
|
|
875
|
+
hostWarning: "Set HOST to your public hostname",
|
|
876
|
+
protocolWarning: "This link will travel unencrypted",
|
|
877
|
+
lastRenderTruncated: true,
|
|
878
|
+
}),
|
|
879
|
+
),
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
render(
|
|
883
|
+
<PersonalCalendarFeedCard
|
|
884
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
885
|
+
now={NOW}
|
|
886
|
+
/>,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
await waitFor(() => {
|
|
890
|
+
expect(
|
|
891
|
+
screen.getByTestId("personal-calendar-feed-host-warning"),
|
|
892
|
+
).toHaveTextContent("Set HOST to your public hostname");
|
|
893
|
+
});
|
|
894
|
+
expect(
|
|
895
|
+
screen.getByTestId("personal-calendar-feed-protocol-warning"),
|
|
896
|
+
).toHaveTextContent("This link will travel unencrypted");
|
|
897
|
+
expect(
|
|
898
|
+
screen.getByTestId("personal-calendar-feed-truncated-warning"),
|
|
899
|
+
).toBeInTheDocument();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("a 404 from an older API hides the feature instead of erroring", async () => {
|
|
903
|
+
getMock.mockResolvedValue(fail(404, "Not found"));
|
|
904
|
+
|
|
905
|
+
render(
|
|
906
|
+
<PersonalCalendarFeedCard
|
|
907
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
908
|
+
now={NOW}
|
|
909
|
+
/>,
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
await waitFor(() => {
|
|
913
|
+
expect(
|
|
914
|
+
screen.getByTestId("personal-calendar-feed-unsupported"),
|
|
915
|
+
).toHaveTextContent("does not offer calendar feeds yet");
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
expect(
|
|
919
|
+
screen.queryByTestId("personal-calendar-feed-generate"),
|
|
920
|
+
).not.toBeInTheDocument();
|
|
921
|
+
expect(screen.queryByText("Not found")).not.toBeInTheDocument();
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("any other failure surfaces the server's own message", async () => {
|
|
925
|
+
getMock.mockResolvedValue(fail(500, "Redis is on fire"));
|
|
926
|
+
|
|
927
|
+
render(
|
|
928
|
+
<PersonalCalendarFeedCard
|
|
929
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
930
|
+
now={NOW}
|
|
931
|
+
/>,
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
await waitFor(() => {
|
|
935
|
+
expect(screen.getByText("Redis is on fire")).toBeInTheDocument();
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test("a failed Generate keeps the empty state and shows the reason", async () => {
|
|
940
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
941
|
+
postMock.mockResolvedValue(
|
|
942
|
+
fail(402, "Upgrade to Growth to use calendar feeds"),
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
render(
|
|
946
|
+
<PersonalCalendarFeedCard
|
|
947
|
+
variant={PersonalCalendarFeedVariant.Full}
|
|
948
|
+
now={NOW}
|
|
949
|
+
/>,
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
await waitFor(() => {
|
|
953
|
+
expect(
|
|
954
|
+
screen.getByTestId("personal-calendar-feed-generate"),
|
|
955
|
+
).toBeInTheDocument();
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
fireEvent.click(screen.getByTestId("personal-calendar-feed-generate"));
|
|
959
|
+
|
|
960
|
+
await waitFor(() => {
|
|
961
|
+
expect(
|
|
962
|
+
screen.getByText("Upgrade to Growth to use calendar feeds"),
|
|
963
|
+
).toBeInTheDocument();
|
|
964
|
+
});
|
|
965
|
+
expect(
|
|
966
|
+
screen.getByTestId("personal-calendar-feed-empty"),
|
|
967
|
+
).toBeInTheDocument();
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
describe("PersonalCalendarFeedCard (schedule variant)", () => {
|
|
972
|
+
beforeEach(() => {
|
|
973
|
+
resetMocks();
|
|
974
|
+
goToCalendarFeedPage();
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
afterEach(() => {
|
|
978
|
+
cleanup();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test("narrows the personal link to the schedule and points at the full page", async () => {
|
|
982
|
+
getMock.mockResolvedValue(ok(activeStatusJson()));
|
|
983
|
+
|
|
984
|
+
render(
|
|
985
|
+
<PersonalCalendarFeedCard
|
|
986
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
987
|
+
scheduleId={SCHEDULE_ID}
|
|
988
|
+
now={NOW}
|
|
989
|
+
/>,
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
await waitFor(() => {
|
|
993
|
+
expect(
|
|
994
|
+
screen.getByTestId("schedule-personal-calendar-feed-links"),
|
|
995
|
+
).toBeInTheDocument();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
expect(
|
|
999
|
+
screen.getByTestId("schedule-personal-calendar-feed-webcal"),
|
|
1000
|
+
).toHaveAttribute(
|
|
1001
|
+
"href",
|
|
1002
|
+
`${WEBCAL_URL}?schedule=${SCHEDULE_ID.toString()}`,
|
|
1003
|
+
);
|
|
1004
|
+
// The page renders the refresh alert once, so the schedule half does not.
|
|
1005
|
+
expect(
|
|
1006
|
+
screen.queryByTestId("schedule-personal-calendar-feed-refresh-alert"),
|
|
1007
|
+
).not.toBeInTheDocument();
|
|
1008
|
+
|
|
1009
|
+
const expectedRoute: Route = RouteUtil.populateRouteParams(
|
|
1010
|
+
RouteMap[PageMap.USER_SETTINGS_ON_CALL_CALENDAR_FEED] as Route,
|
|
1011
|
+
);
|
|
1012
|
+
expect(
|
|
1013
|
+
screen.getByText("Manage your calendar link and reminders").closest("a"),
|
|
1014
|
+
).toHaveAttribute("href", expectedRoute.toString());
|
|
1015
|
+
|
|
1016
|
+
// No regenerate / disable / delete controls in the narrow variant.
|
|
1017
|
+
expect(screen.queryByTestId("card-button")).not.toBeInTheDocument();
|
|
1018
|
+
expect(
|
|
1019
|
+
screen.queryByTestId("card-model-detail-stub"),
|
|
1020
|
+
).not.toBeInTheDocument();
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
/*
|
|
1024
|
+
* A truncated render is a property of the feed, not of the page it is shown
|
|
1025
|
+
* on: the same link is short on the schedule page and on the settings page.
|
|
1026
|
+
* The narrow variant used to drop the flag, so a reader who only ever opens
|
|
1027
|
+
* the schedule page never learned their calendar was incomplete.
|
|
1028
|
+
*/
|
|
1029
|
+
test("the narrow variant carries the truncation warning, like the full one", async () => {
|
|
1030
|
+
getMock.mockResolvedValue(
|
|
1031
|
+
ok(activeStatusJson({ lastRenderTruncated: true })),
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
render(
|
|
1035
|
+
<PersonalCalendarFeedCard
|
|
1036
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
1037
|
+
scheduleId={SCHEDULE_ID}
|
|
1038
|
+
now={NOW}
|
|
1039
|
+
/>,
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await waitFor(() => {
|
|
1043
|
+
expect(
|
|
1044
|
+
screen.getByTestId("schedule-personal-calendar-feed-links"),
|
|
1045
|
+
).toBeInTheDocument();
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
expect(
|
|
1049
|
+
screen.getByTestId("schedule-personal-calendar-feed-truncated-warning"),
|
|
1050
|
+
).toHaveTextContent("shortened because it would have exceeded the event");
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("offers Generate when no link exists and posts to /feed/rotate", async () => {
|
|
1054
|
+
getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
|
|
1055
|
+
postMock.mockResolvedValue(ok(activeStatusJson()));
|
|
1056
|
+
|
|
1057
|
+
render(
|
|
1058
|
+
<PersonalCalendarFeedCard
|
|
1059
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
1060
|
+
scheduleId={SCHEDULE_ID}
|
|
1061
|
+
now={NOW}
|
|
1062
|
+
/>,
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
await waitFor(() => {
|
|
1066
|
+
expect(
|
|
1067
|
+
screen.getByTestId("schedule-personal-calendar-feed-generate"),
|
|
1068
|
+
).toBeInTheDocument();
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
fireEvent.click(
|
|
1072
|
+
screen.getByTestId("schedule-personal-calendar-feed-generate"),
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
await waitFor(() => {
|
|
1076
|
+
expect(
|
|
1077
|
+
screen.getByTestId("schedule-personal-calendar-feed-links"),
|
|
1078
|
+
).toBeInTheDocument();
|
|
1079
|
+
});
|
|
1080
|
+
expect(urlOf(postMock.mock.calls[0] as Array<unknown>)).toContain(
|
|
1081
|
+
PERSONAL_FEED_ROTATE_PATH,
|
|
1082
|
+
);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("a disabled link explains itself rather than showing a dead URL", async () => {
|
|
1086
|
+
getMock.mockResolvedValue(ok(activeStatusJson({ isEnabled: false })));
|
|
1087
|
+
|
|
1088
|
+
render(
|
|
1089
|
+
<PersonalCalendarFeedCard
|
|
1090
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
1091
|
+
scheduleId={SCHEDULE_ID}
|
|
1092
|
+
now={NOW}
|
|
1093
|
+
/>,
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
await waitFor(() => {
|
|
1097
|
+
expect(screen.getByText(/This link is disabled/)).toBeInTheDocument();
|
|
1098
|
+
});
|
|
1099
|
+
expect(
|
|
1100
|
+
screen.queryByTestId("schedule-personal-calendar-feed-links"),
|
|
1101
|
+
).not.toBeInTheDocument();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test("an unreadable link says it needs regenerating", async () => {
|
|
1105
|
+
getMock.mockResolvedValue(
|
|
1106
|
+
ok(activeStatusJson({ needsRegeneration: true, urls: null })),
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
render(
|
|
1110
|
+
<PersonalCalendarFeedCard
|
|
1111
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
1112
|
+
scheduleId={SCHEDULE_ID}
|
|
1113
|
+
now={NOW}
|
|
1114
|
+
/>,
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
await waitFor(() => {
|
|
1118
|
+
expect(
|
|
1119
|
+
screen.getByText(/needs to be regenerated before it can be shown here/),
|
|
1120
|
+
).toBeInTheDocument();
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("a 404 from an older API renders the unsupported note", async () => {
|
|
1125
|
+
getMock.mockResolvedValue(fail(404, "Not found"));
|
|
1126
|
+
|
|
1127
|
+
render(
|
|
1128
|
+
<PersonalCalendarFeedCard
|
|
1129
|
+
variant={PersonalCalendarFeedVariant.Schedule}
|
|
1130
|
+
scheduleId={SCHEDULE_ID}
|
|
1131
|
+
now={NOW}
|
|
1132
|
+
/>,
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
await waitFor(() => {
|
|
1136
|
+
expect(
|
|
1137
|
+
screen.getByTestId("schedule-personal-calendar-feed-unsupported"),
|
|
1138
|
+
).toBeInTheDocument();
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
describe("User Settings > Calendar Feed page", () => {
|
|
1144
|
+
beforeEach(() => {
|
|
1145
|
+
resetMocks();
|
|
1146
|
+
goToCalendarFeedPage();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
afterEach(() => {
|
|
1150
|
+
cleanup();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test("renders the link card, the upcoming shifts card and the reminders card", async () => {
|
|
1154
|
+
getMock.mockImplementation((args: Record<string, unknown>) => {
|
|
1155
|
+
const url: string = String(args["url"]);
|
|
1156
|
+
|
|
1157
|
+
if (url.includes("/my-shifts")) {
|
|
1158
|
+
return Promise.resolve(
|
|
1159
|
+
ok({ shifts: [], truncated: false, generatedAt: NOW.toISOString() }),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return Promise.resolve(ok(activeStatusJson()));
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
render(
|
|
1167
|
+
<UserSettingsOnCallCalendarFeed
|
|
1168
|
+
pageRoute={new Route("/")}
|
|
1169
|
+
currentProject={null}
|
|
1170
|
+
hasPaymentMethod={false}
|
|
1171
|
+
/>,
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
await waitFor(() => {
|
|
1175
|
+
expect(
|
|
1176
|
+
screen.getByTestId("personal-calendar-feed-active"),
|
|
1177
|
+
).toBeInTheDocument();
|
|
1178
|
+
});
|
|
1179
|
+
await waitFor(() => {
|
|
1180
|
+
expect(screen.getByTestId("upcoming-shifts-empty")).toBeInTheDocument();
|
|
1181
|
+
});
|
|
1182
|
+
await waitFor(() => {
|
|
1183
|
+
expect(
|
|
1184
|
+
screen.getByTestId("shift-reminder-chip-custom"),
|
|
1185
|
+
).toBeInTheDocument();
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
expect(
|
|
1189
|
+
screen.getByText("Subscribe to your on-call shifts"),
|
|
1190
|
+
).toBeInTheDocument();
|
|
1191
|
+
expect(
|
|
1192
|
+
screen.getByText("Upcoming shifts (next 30 days)"),
|
|
1193
|
+
).toBeInTheDocument();
|
|
1194
|
+
expect(screen.getByText("Remind me before shifts")).toBeInTheDocument();
|
|
1195
|
+
|
|
1196
|
+
// The reminders card asked for the signed-in user's rows in this project.
|
|
1197
|
+
expect(getListMock).toHaveBeenCalledTimes(1);
|
|
1198
|
+
const listCall: Record<string, unknown> = getListMock.mock
|
|
1199
|
+
.calls[0]![0] as Record<string, unknown>;
|
|
1200
|
+
const query: Record<string, unknown> = listCall["query"] as Record<
|
|
1201
|
+
string,
|
|
1202
|
+
unknown
|
|
1203
|
+
>;
|
|
1204
|
+
expect(String(query["projectId"])).toBe(PROJECT_ID);
|
|
1205
|
+
expect(String(query["userId"])).toBe(
|
|
1206
|
+
"44444444-4444-4444-8444-444444444444",
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
});
|