@oneuptime/common 14.0.7 → 14.0.8
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/Incident.ts +267 -1
- package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
- package/Models/DatabaseModels/StatusPage.ts +55 -0
- package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
- package/Server/API/IncidentAPI.ts +162 -0
- package/Server/API/StatusPageAPI.ts +487 -171
- package/Server/API/StatusPageSubscriberAPI.ts +63 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Infrastructure/Semaphore.ts +10 -0
- package/Server/Middleware/IdentityRateLimit.ts +108 -3
- package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
- package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
- package/Server/Services/DatabaseService.ts +93 -10
- package/Server/Services/IncidentCustomFieldService.ts +276 -1
- package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
- package/Server/Services/IncidentEpisodeService.ts +15 -0
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
- package/Server/Services/IncidentPublicNoteService.ts +30 -1
- package/Server/Services/IncidentService.ts +1535 -65
- package/Server/Services/IncidentStateTimelineService.ts +13 -0
- package/Server/Services/IncidentTemplateService.ts +109 -0
- package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
- package/Server/Services/ScheduledMaintenanceService.ts +68 -18
- package/Server/Services/SmsService.ts +10 -0
- package/Server/Services/StatusPageService.ts +65 -8
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
- package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
- package/Server/Services/StatusPageSubscriberService.ts +1161 -64
- package/Server/Types/Markdown.ts +153 -6
- package/Server/Types/Workflow/Components/Email.ts +61 -2
- package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
- package/Server/Utils/DataSource/EgressGuard.ts +66 -21
- package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
- package/Server/Utils/SSRFProtection.ts +76 -16
- package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
- package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
- package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
- package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
- package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
- package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
- package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
- package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
- package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
- package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
- package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
- package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
- package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
- package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
- package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
- package/Server/Utils/StatusPageResource.ts +51 -20
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
- package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
- package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
- package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
- package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
- package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
- package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
- package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
- package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
- package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
- package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
- package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
- package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
- package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
- package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
- package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
- package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
- package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
- package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
- package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
- package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
- package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
- package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
- package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
- package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
- package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
- package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
- package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
- package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
- package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
- package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
- package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
- package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
- package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
- package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
- package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
- package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
- package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
- package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
- package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
- package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
- package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
- package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
- package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
- package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
- package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
- package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
- package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
- package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
- package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
- package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
- package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
- package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
- package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
- package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
- package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
- package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
- package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
- package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
- package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
- package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
- package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
- package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
- package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
- package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
- package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
- package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
- package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
- package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
- package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
- package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
- package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
- package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
- package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
- package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
- package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
- package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
- package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
- package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
- package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
- package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
- package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
- package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
- package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
- package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
- package/Tests/Types/SafeHtml.test.ts +102 -0
- package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
- package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
- package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
- package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
- package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
- package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
- package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
- package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
- package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
- package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
- package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
- package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
- package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
- package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
- package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
- package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
- package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
- package/Types/CustomField/CustomFieldOrder.ts +55 -0
- package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
- package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
- package/Types/CustomField/CustomFieldType.ts +14 -0
- package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
- package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
- package/Types/SafeHtml.ts +93 -0
- package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
- package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
- package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
- package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
- package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
- package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
- package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
- package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
- package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
- package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
- package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
- package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
- package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
- package/UI/Components/Forms/ModelForm.tsx +70 -8
- package/UI/Components/Forms/Validation.ts +1 -1
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
- package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
- package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
- package/UI/Components/ModelTable/ModelTable.tsx +6 -7
- package/UI/Utils/SensitiveUrlToken.ts +10 -5
- package/Utils/API.ts +247 -2
- package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
- package/Utils/Markdown/MarkdownEscape.ts +65 -0
- package/Utils/ProbeNetworkFailureUtil.ts +673 -0
- package/build/dist/Models/DatabaseModels/Incident.js +267 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/API/IncidentAPI.js +100 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +417 -176
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
- package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
- package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
- package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +56 -7
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
- package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1052 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
- package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/SmsService.js +2 -22
- package/build/dist/Server/Services/SmsService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +47 -9
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +110 -6
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
- package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +48 -6
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
- package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
- package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
- package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
- package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
- package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageResource.js +39 -7
- package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
- package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
- package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
- package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
- package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
- package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
- package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
- package/build/dist/Types/SafeHtml.js +84 -0
- package/build/dist/Types/SafeHtml.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
- package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
- package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
- package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
- package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
- package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
- package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +1 -1
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
- package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
- package/build/dist/Utils/API.js +177 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
- package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
- package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
- package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
- package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
import StatusPage from "../../../../Models/DatabaseModels/StatusPage";
|
|
2
|
+
import { StatusPageExclusionReason } from "../../../../Server/Utils/StatusPage/StatusPageExclusion";
|
|
3
|
+
import SubscriberNotificationDeliveryRecord, {
|
|
4
|
+
StatusPageDeliverySkipReason,
|
|
5
|
+
SubscriberNotificationRetryScope,
|
|
6
|
+
} from "../../../../Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord";
|
|
7
|
+
import SubscriberNotificationTiming from "../../../../Server/Utils/StatusPage/SubscriberNotificationTiming";
|
|
8
|
+
import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
|
|
9
|
+
import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
|
10
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
11
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
12
|
+
import Email from "../../../../Types/Email";
|
|
13
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
14
|
+
import Phone from "../../../../Types/Phone";
|
|
15
|
+
import StatusPageSubscriberNotificationMethod from "../../../../Types/StatusPage/StatusPageSubscriberNotificationMethod";
|
|
16
|
+
import { escapeMarkdownInline } from "../../../../Utils/Markdown/MarkdownEscape";
|
|
17
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
18
|
+
import type { SpyInstance } from "jest-mock";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The per-send record the incident and episode subscriber jobs keep: it
|
|
22
|
+
* decides whether an email or SMS goes to an address already sent it in this
|
|
23
|
+
* send (only for scoped incidents), and it writes what the send did on each
|
|
24
|
+
* status page for the feed item.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const SITE_03: string = "b0000000-0000-4000-8000-000000000003";
|
|
28
|
+
const SITE_07: string = "b0000000-0000-4000-8000-000000000007";
|
|
29
|
+
const SITE_09: string = "b0000000-0000-4000-8000-000000000009";
|
|
30
|
+
|
|
31
|
+
function page(id: string, name: string, pageTitle?: string): StatusPage {
|
|
32
|
+
const statusPage: StatusPage = new StatusPage();
|
|
33
|
+
statusPage._id = id;
|
|
34
|
+
statusPage.name = name;
|
|
35
|
+
|
|
36
|
+
if (pageTitle) {
|
|
37
|
+
statusPage.pageTitle = pageTitle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return statusPage;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const site03: StatusPage = page(SITE_03, "Site 03");
|
|
44
|
+
const site07: StatusPage = page(SITE_07, "Site 07");
|
|
45
|
+
const site09: StatusPage = page(SITE_09, "Site 09");
|
|
46
|
+
|
|
47
|
+
describe("SubscriberNotificationDeliveryRecord email and SMS dedupe", () => {
|
|
48
|
+
test("for a scoped send, an address gets one email however many pages it is on", () => {
|
|
49
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
50
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
record.shouldSendEmail({
|
|
54
|
+
statusPage: site03,
|
|
55
|
+
email: new Email("manager@acme.com"),
|
|
56
|
+
}),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
expect(
|
|
59
|
+
record.shouldSendEmail({
|
|
60
|
+
statusPage: site07,
|
|
61
|
+
email: new Email("manager@acme.com"),
|
|
62
|
+
}),
|
|
63
|
+
).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("addresses match whatever their case and surrounding space", () => {
|
|
67
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
68
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
record.shouldSendEmail({
|
|
72
|
+
statusPage: site03,
|
|
73
|
+
email: "Manager@Acme.com ",
|
|
74
|
+
}),
|
|
75
|
+
).toBe(true);
|
|
76
|
+
expect(
|
|
77
|
+
record.shouldSendEmail({ statusPage: site07, email: "manager@acme.com" }),
|
|
78
|
+
).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("phone numbers match however they are formatted", () => {
|
|
82
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
83
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
record.shouldSendSms({
|
|
87
|
+
statusPage: site03,
|
|
88
|
+
phone: new Phone("+15555550100"),
|
|
89
|
+
}),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
expect(
|
|
92
|
+
record.shouldSendSms({ statusPage: site07, phone: "+1 (555) 555-0100" }),
|
|
93
|
+
).toBe(false);
|
|
94
|
+
// A different number still gets its SMS.
|
|
95
|
+
expect(
|
|
96
|
+
record.shouldSendSms({ statusPage: site07, phone: "+15555550101" }),
|
|
97
|
+
).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a leading + is part of the number", () => {
|
|
101
|
+
expect(
|
|
102
|
+
SubscriberNotificationDeliveryRecord.normalizePhone("+1 555 0100"),
|
|
103
|
+
).toBe("+15550100");
|
|
104
|
+
expect(
|
|
105
|
+
SubscriberNotificationDeliveryRecord.normalizePhone("1 555 0100"),
|
|
106
|
+
).toBe("15550100");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("email and SMS are deduplicated separately", () => {
|
|
110
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
111
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
record.shouldSendEmail({ statusPage: site03, email: "a@acme.com" }),
|
|
115
|
+
).toBe(true);
|
|
116
|
+
expect(
|
|
117
|
+
record.shouldSendSms({ statusPage: site03, phone: "+15555550100" }),
|
|
118
|
+
).toBe(true);
|
|
119
|
+
expect(
|
|
120
|
+
record.shouldSendSms({ statusPage: site07, phone: "+15555550100" }),
|
|
121
|
+
).toBe(false);
|
|
122
|
+
expect(
|
|
123
|
+
record.shouldSendEmail({ statusPage: site07, email: "b@acme.com" }),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("for an unscoped send, every subscription gets its message, as before", () => {
|
|
128
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
129
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
130
|
+
|
|
131
|
+
for (const statusPage of [site03, site07, site09]) {
|
|
132
|
+
expect(
|
|
133
|
+
record.shouldSendEmail({ statusPage, email: "manager@acme.com" }),
|
|
134
|
+
).toBe(true);
|
|
135
|
+
expect(record.shouldSendSms({ statusPage, phone: "+15555550100" })).toBe(
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
expect(record.toMarkdown()).not.toContain("Not sent again");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* An address is taken when a page asks to send to it, and counts as sent
|
|
145
|
+
* only once its message went out. When the first page's message fails -
|
|
146
|
+
* that page's own SMTP or Twilio is misconfigured - the next page that has
|
|
147
|
+
* the address sends it, rather than counting it as already sent.
|
|
148
|
+
*/
|
|
149
|
+
test("an address whose email failed on one page is sent on the next page that has it", async () => {
|
|
150
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
151
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
record.shouldSendEmail({ statusPage: site03, email: "manager@acme.com" }),
|
|
155
|
+
).toBe(true);
|
|
156
|
+
await record.deliver({
|
|
157
|
+
statusPage: site03,
|
|
158
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
159
|
+
to: new Email("manager@acme.com"),
|
|
160
|
+
send: () => {
|
|
161
|
+
return Promise.resolve(
|
|
162
|
+
new HTTPErrorResponse(500, { message: "SMTP down" }, {}),
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// A differently written form of the same address is the same address.
|
|
168
|
+
expect(
|
|
169
|
+
record.shouldSendEmail({
|
|
170
|
+
statusPage: site07,
|
|
171
|
+
email: " Manager@Acme.com",
|
|
172
|
+
}),
|
|
173
|
+
).toBe(true);
|
|
174
|
+
await record.deliver({
|
|
175
|
+
statusPage: site07,
|
|
176
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
177
|
+
to: " Manager@Acme.com",
|
|
178
|
+
send: () => {
|
|
179
|
+
return Promise.resolve(undefined);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Sent now: a third page does not send it again.
|
|
184
|
+
expect(
|
|
185
|
+
record.shouldSendEmail({ statusPage: site09, email: "manager@acme.com" }),
|
|
186
|
+
).toBe(false);
|
|
187
|
+
|
|
188
|
+
expect(record.getFailedCount(SITE_03)).toBe(1);
|
|
189
|
+
expect(record.getSentCount(SITE_07)).toBe(1);
|
|
190
|
+
// Site 07 is not credited with an address "already sent it".
|
|
191
|
+
expect(record.toMarkdown()).not.toContain(
|
|
192
|
+
"- **Site 07**: 1 email sent. Not sent again",
|
|
193
|
+
);
|
|
194
|
+
expect(record.toMarkdown()).toContain(
|
|
195
|
+
"- **Site 09**: nothing sent. Not sent again to 1 email address already sent it through another status page.",
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a phone number whose SMS failed on one page is sent on the next", async () => {
|
|
200
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
201
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
202
|
+
|
|
203
|
+
expect(
|
|
204
|
+
record.shouldSendSms({ statusPage: site03, phone: "+15555550100" }),
|
|
205
|
+
).toBe(true);
|
|
206
|
+
await record.deliver({
|
|
207
|
+
statusPage: site03,
|
|
208
|
+
method: StatusPageSubscriberNotificationMethod.SMS,
|
|
209
|
+
to: new Phone("+15555550100"),
|
|
210
|
+
send: () => {
|
|
211
|
+
return Promise.reject(new Error("Twilio refused"));
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
record.shouldSendSms({ statusPage: site07, phone: "+1 (555) 555-0100" }),
|
|
217
|
+
).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("an address whose message went out stays taken", async () => {
|
|
221
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
222
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
223
|
+
|
|
224
|
+
record.shouldSendEmail({ statusPage: site03, email: "manager@acme.com" });
|
|
225
|
+
await record.deliver({
|
|
226
|
+
statusPage: site03,
|
|
227
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
228
|
+
to: "manager@acme.com",
|
|
229
|
+
send: () => {
|
|
230
|
+
return Promise.resolve(undefined);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
record.releaseUndeliveredAddresses(site03);
|
|
234
|
+
|
|
235
|
+
expect(
|
|
236
|
+
record.shouldSendEmail({ statusPage: site07, email: "manager@acme.com" }),
|
|
237
|
+
).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("an address a page reserved and never sent to is freed once the page is done", () => {
|
|
241
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
242
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
243
|
+
|
|
244
|
+
// Site 03's handler took the address, then threw before sending.
|
|
245
|
+
expect(
|
|
246
|
+
record.shouldSendEmail({ statusPage: site03, email: "manager@acme.com" }),
|
|
247
|
+
).toBe(true);
|
|
248
|
+
expect(
|
|
249
|
+
record.shouldSendSms({ statusPage: site03, phone: "+15555550100" }),
|
|
250
|
+
).toBe(true);
|
|
251
|
+
|
|
252
|
+
// Still taken while Site 03 is being sent.
|
|
253
|
+
expect(
|
|
254
|
+
record.shouldSendEmail({ statusPage: site03, email: "manager@acme.com" }),
|
|
255
|
+
).toBe(false);
|
|
256
|
+
|
|
257
|
+
record.releaseUndeliveredAddresses(site07);
|
|
258
|
+
expect(
|
|
259
|
+
record.shouldSendEmail({ statusPage: site07, email: "manager@acme.com" }),
|
|
260
|
+
).toBe(false);
|
|
261
|
+
|
|
262
|
+
record.releaseUndeliveredAddresses(site03);
|
|
263
|
+
expect(
|
|
264
|
+
record.shouldSendEmail({ statusPage: site07, email: "manager@acme.com" }),
|
|
265
|
+
).toBe(true);
|
|
266
|
+
expect(
|
|
267
|
+
record.shouldSendSms({ statusPage: site07, phone: "+15555550100" }),
|
|
268
|
+
).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("for an unscoped send, a failed message frees nothing, since nothing is taken", async () => {
|
|
272
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
273
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
274
|
+
|
|
275
|
+
await record.deliver({
|
|
276
|
+
statusPage: site03,
|
|
277
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
278
|
+
to: "manager@acme.com",
|
|
279
|
+
send: () => {
|
|
280
|
+
return Promise.reject(new Error("SMTP down"));
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(
|
|
285
|
+
record.shouldSendEmail({ statusPage: site07, email: "manager@acme.com" }),
|
|
286
|
+
).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("addresses are remembered for one send only", () => {
|
|
290
|
+
const first: SubscriberNotificationDeliveryRecord =
|
|
291
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
292
|
+
first.shouldSendEmail({ statusPage: site03, email: "a@acme.com" });
|
|
293
|
+
|
|
294
|
+
const second: SubscriberNotificationDeliveryRecord =
|
|
295
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
296
|
+
|
|
297
|
+
expect(
|
|
298
|
+
second.shouldSendEmail({ statusPage: site07, email: "a@acme.com" }),
|
|
299
|
+
).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("SubscriberNotificationDeliveryRecord counts", () => {
|
|
304
|
+
test("counts what was sent on each page and channel", () => {
|
|
305
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
306
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
307
|
+
|
|
308
|
+
record.startStatusPage(site03);
|
|
309
|
+
record.recordSent({
|
|
310
|
+
statusPage: site03,
|
|
311
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
312
|
+
subject: "[Incident] Checkout down",
|
|
313
|
+
});
|
|
314
|
+
record.recordSent({
|
|
315
|
+
statusPage: site03,
|
|
316
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
317
|
+
subject: "[Incident] Something else",
|
|
318
|
+
});
|
|
319
|
+
record.recordSent({
|
|
320
|
+
statusPage: site03,
|
|
321
|
+
method: StatusPageSubscriberNotificationMethod.Webhook,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(record.getSentCount(SITE_03)).toBe(3);
|
|
325
|
+
expect(record.getSentCount(new ObjectID(SITE_03.toUpperCase()))).toBe(3);
|
|
326
|
+
expect(
|
|
327
|
+
record.getSentCount(
|
|
328
|
+
SITE_03,
|
|
329
|
+
StatusPageSubscriberNotificationMethod.Email,
|
|
330
|
+
),
|
|
331
|
+
).toBe(2);
|
|
332
|
+
// The first subject sent on the page is the one recorded.
|
|
333
|
+
expect(record.getSubject(SITE_03)).toBe("[Incident] Checkout down");
|
|
334
|
+
expect(record.getSentCount(SITE_07)).toBe(0);
|
|
335
|
+
expect(record.getFailedCount(SITE_03)).toBe(0);
|
|
336
|
+
expect(record.hasAttemptedAny()).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a subject carrying a subscriber's unsubscribe link is recorded with its token redacted", () => {
|
|
340
|
+
/*
|
|
341
|
+
* A custom subject template can use {{unsubscribeUrl}}. The feed is read
|
|
342
|
+
* by everyone who can read the incident; the token belongs to the one
|
|
343
|
+
* subscriber the email went to, and cancels its subscription.
|
|
344
|
+
*/
|
|
345
|
+
const token: string = "3e".repeat(32);
|
|
346
|
+
const subscriberId: string = "c0000000-0000-4000-8000-000000000001";
|
|
347
|
+
|
|
348
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
349
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
350
|
+
|
|
351
|
+
record.startStatusPage(site03);
|
|
352
|
+
record.recordSent({
|
|
353
|
+
statusPage: site03,
|
|
354
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
355
|
+
subject: `Checkout down - stop: https://status.acme.com/unsubscribe/${subscriberId}-${token}`,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(record.getSubject(SITE_03)).toBe(
|
|
359
|
+
`Checkout down - stop: https://status.acme.com/unsubscribe/${subscriberId}-[redacted]`,
|
|
360
|
+
);
|
|
361
|
+
expect(record.toMarkdown()).not.toContain(token);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("a record with nothing sent or tried says so", () => {
|
|
365
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
366
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
367
|
+
|
|
368
|
+
record.startStatusPage(site03);
|
|
369
|
+
|
|
370
|
+
expect(record.hasAttemptedAny()).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("SubscriberNotificationDeliveryRecord.toMarkdown", () => {
|
|
375
|
+
test("lists each page with what was sent and the subject, in the order visited", () => {
|
|
376
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
377
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
378
|
+
|
|
379
|
+
record.startStatusPage(site07);
|
|
380
|
+
for (let i: number = 0; i < 18; i++) {
|
|
381
|
+
record.recordSent({
|
|
382
|
+
statusPage: site07,
|
|
383
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
384
|
+
subject: "[Incident] Checkout down",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
record.startStatusPage(site03);
|
|
388
|
+
record.recordSent({
|
|
389
|
+
statusPage: site03,
|
|
390
|
+
method: StatusPageSubscriberNotificationMethod.SMS,
|
|
391
|
+
});
|
|
392
|
+
record.recordSent({
|
|
393
|
+
statusPage: site03,
|
|
394
|
+
method: StatusPageSubscriberNotificationMethod.Slack,
|
|
395
|
+
});
|
|
396
|
+
record.recordSent({
|
|
397
|
+
statusPage: site03,
|
|
398
|
+
method: StatusPageSubscriberNotificationMethod.MicrosoftTeams,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(record.toMarkdown()).toBe(
|
|
402
|
+
[
|
|
403
|
+
"**Status pages:**",
|
|
404
|
+
"",
|
|
405
|
+
'- **Site 07**: 18 email sent. Subject: "\\[Incident\\] Checkout down".',
|
|
406
|
+
"- **Site 03**: 1 SMS, 1 Slack, 1 Microsoft Teams sent.",
|
|
407
|
+
].join("\n"),
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("says how many addresses were not sent it again, and why", () => {
|
|
412
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
413
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
414
|
+
|
|
415
|
+
record.startStatusPage(site03);
|
|
416
|
+
record.shouldSendEmail({ statusPage: site03, email: "a@acme.com" });
|
|
417
|
+
record.recordSent({
|
|
418
|
+
statusPage: site03,
|
|
419
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
420
|
+
subject: "Down",
|
|
421
|
+
});
|
|
422
|
+
record.shouldSendSms({ statusPage: site03, phone: "+15555550100" });
|
|
423
|
+
record.recordSent({
|
|
424
|
+
statusPage: site03,
|
|
425
|
+
method: StatusPageSubscriberNotificationMethod.SMS,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
record.startStatusPage(site07);
|
|
429
|
+
record.shouldSendEmail({ statusPage: site07, email: "a@acme.com" });
|
|
430
|
+
record.shouldSendSms({ statusPage: site07, phone: "+15555550100" });
|
|
431
|
+
// The webhook on the same page is not deduplicated.
|
|
432
|
+
record.recordSent({
|
|
433
|
+
statusPage: site07,
|
|
434
|
+
method: StatusPageSubscriberNotificationMethod.Webhook,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const markdown: string = record.toMarkdown();
|
|
438
|
+
|
|
439
|
+
expect(markdown).toContain(
|
|
440
|
+
"- **Site 07**: 1 webhook sent. Not sent again to 1 email address and 1 phone number already sent it through another status page.",
|
|
441
|
+
);
|
|
442
|
+
expect(markdown).toContain(
|
|
443
|
+
"Email and SMS were sent once per address across these status pages",
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("names the pages passed over and why", () => {
|
|
448
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
449
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
450
|
+
|
|
451
|
+
record.skipStatusPage(site03, StatusPageDeliverySkipReason.AlreadyNotified);
|
|
452
|
+
record.skipStatusPage(site07, StatusPageDeliverySkipReason.HidesIncidents);
|
|
453
|
+
record.skipStatusPage(site09, StatusPageDeliverySkipReason.HidesEpisodes);
|
|
454
|
+
|
|
455
|
+
expect(record.toMarkdown()).toBe(
|
|
456
|
+
[
|
|
457
|
+
"**Status pages:**",
|
|
458
|
+
"",
|
|
459
|
+
"- **Site 03**: not sent again, its subscribers were already sent this notification.",
|
|
460
|
+
"- **Site 07**: not sent, this status page does not show incidents.",
|
|
461
|
+
"- **Site 09**: not sent, this status page does not show episodes.",
|
|
462
|
+
].join("\n"),
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("a page that failed part-way keeps its counts and says it failed", () => {
|
|
467
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
468
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
469
|
+
|
|
470
|
+
record.startStatusPage(site03);
|
|
471
|
+
record.recordSent({
|
|
472
|
+
statusPage: site03,
|
|
473
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
474
|
+
subject: "Down",
|
|
475
|
+
});
|
|
476
|
+
record.skipStatusPage(site03, StatusPageDeliverySkipReason.Failed);
|
|
477
|
+
|
|
478
|
+
record.skipStatusPage(site07, StatusPageDeliverySkipReason.Failed);
|
|
479
|
+
|
|
480
|
+
const markdown: string = record.toMarkdown();
|
|
481
|
+
|
|
482
|
+
expect(markdown).toContain(
|
|
483
|
+
'- **Site 03**: 1 email sent. Sending to this status page failed part-way, so some of its subscribers may not have been sent it. Subject: "Down".',
|
|
484
|
+
);
|
|
485
|
+
expect(markdown).toContain(
|
|
486
|
+
"- **Site 07**: nothing sent. Sending to this status page failed part-way",
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("a page whose every address was already sent it says nothing new was sent", () => {
|
|
491
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
492
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
493
|
+
|
|
494
|
+
record.shouldSendEmail({ statusPage: site03, email: "a@acme.com" });
|
|
495
|
+
record.recordSent({
|
|
496
|
+
statusPage: site03,
|
|
497
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
498
|
+
});
|
|
499
|
+
record.startStatusPage(site07);
|
|
500
|
+
record.shouldSendEmail({ statusPage: site07, email: "a@acme.com" });
|
|
501
|
+
|
|
502
|
+
expect(record.toMarkdown()).toContain(
|
|
503
|
+
"- **Site 07**: nothing sent. Not sent again to 1 email address already sent it through another status page.",
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("a page where no subscriber matched says so", () => {
|
|
508
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
509
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
510
|
+
|
|
511
|
+
record.startStatusPage(site09);
|
|
512
|
+
|
|
513
|
+
expect(record.toMarkdown()).toContain(
|
|
514
|
+
"- **Site 09**: nothing sent, no subscriber matched.",
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("names the pages the scope left out, by reason", () => {
|
|
519
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
520
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
521
|
+
|
|
522
|
+
record.addExcludedStatusPages([
|
|
523
|
+
{
|
|
524
|
+
statusPage: site07,
|
|
525
|
+
reason: StatusPageExclusionReason.OutsideIncidentScope,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
statusPage: site09,
|
|
529
|
+
reason: StatusPageExclusionReason.OnlyShowsScopedIncidents,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
statusPage: site03,
|
|
533
|
+
reason: StatusPageExclusionReason.OutsideIncidentScope,
|
|
534
|
+
},
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
expect(record.toMarkdown()).toBe(
|
|
538
|
+
[
|
|
539
|
+
"**Not sent to 2 status pages outside the status pages this is limited to:** Site 07, Site 03.",
|
|
540
|
+
"**Not sent to 1 status page that only shows incidents limited to it:** Site 09.",
|
|
541
|
+
].join("\n\n"),
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("a long list of left-out pages is cut short", () => {
|
|
546
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
547
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
548
|
+
|
|
549
|
+
record.addExcludedStatusPages(
|
|
550
|
+
Array.from({ length: 30 }, (_v: unknown, i: number) => {
|
|
551
|
+
return {
|
|
552
|
+
statusPage: page(
|
|
553
|
+
`b0000000-0000-4000-8000-0000000001${i.toString().padStart(2, "0")}`,
|
|
554
|
+
`Site ${i + 1}`,
|
|
555
|
+
),
|
|
556
|
+
reason: StatusPageExclusionReason.OnlyShowsScopedIncidents,
|
|
557
|
+
};
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const markdown: string = record.toMarkdown();
|
|
562
|
+
|
|
563
|
+
expect(markdown).toContain("**Not sent to 30 status pages");
|
|
564
|
+
expect(markdown).toContain("Site 25, and 5 more.");
|
|
565
|
+
expect(markdown).not.toContain("Site 26");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("page names and subjects cannot turn into Markdown or break the line", () => {
|
|
569
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
570
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
571
|
+
|
|
572
|
+
const tricky: StatusPage = page(SITE_03, "*Internal* [ops]");
|
|
573
|
+
record.startStatusPage(tricky);
|
|
574
|
+
record.recordSent({
|
|
575
|
+
statusPage: tricky,
|
|
576
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
577
|
+
subject: "Line one\nLine _two_ <b>",
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const markdown: string = record.toMarkdown();
|
|
581
|
+
|
|
582
|
+
expect(markdown).toContain("**\\*Internal\\* \\[ops\\]**");
|
|
583
|
+
expect(markdown).toContain('Subject: "Line one Line \\_two\\_ \\<b\\>".');
|
|
584
|
+
expect(markdown.split("\n")).toHaveLength(3);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
/*
|
|
588
|
+
* The jobs put this record and the custom field values sent into one feed
|
|
589
|
+
* item, and the custom field half escapes with the shared helper
|
|
590
|
+
* (escapeMarkdownInline). Both halves escape by the same rule, so a name
|
|
591
|
+
* is never a link or an image in one half and text in the other.
|
|
592
|
+
*/
|
|
593
|
+
test("escapes page names, excluded pages and subjects as the shared feed helper does", () => {
|
|
594
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
595
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
596
|
+
|
|
597
|
+
const name: string =
|
|
598
|
+
"Site 3](https://evil.example) ";
|
|
599
|
+
const tricky: StatusPage = page(SITE_03, name);
|
|
600
|
+
record.startStatusPage(tricky);
|
|
601
|
+
record.recordSent({
|
|
602
|
+
statusPage: tricky,
|
|
603
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
604
|
+
subject: "[Acme] Checkout - down (EU)!",
|
|
605
|
+
});
|
|
606
|
+
record.addExcludedStatusPages([
|
|
607
|
+
{
|
|
608
|
+
statusPage: page(SITE_07, "Site 07 "),
|
|
609
|
+
reason: StatusPageExclusionReason.OutsideIncidentScope,
|
|
610
|
+
},
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
const markdown: string = record.toMarkdown();
|
|
614
|
+
|
|
615
|
+
expect(markdown).toContain(`**${escapeMarkdownInline(name)}**`);
|
|
616
|
+
expect(markdown).toContain(
|
|
617
|
+
`Subject: "${escapeMarkdownInline("[Acme] Checkout - down (EU)!")}".`,
|
|
618
|
+
);
|
|
619
|
+
expect(markdown).toContain(
|
|
620
|
+
escapeMarkdownInline("Site 07 "),
|
|
621
|
+
);
|
|
622
|
+
expect(markdown).not.toContain("](https://");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("a page goes by its name, then its public title", () => {
|
|
626
|
+
expect(
|
|
627
|
+
SubscriberNotificationDeliveryRecord.getStatusPageName(
|
|
628
|
+
page(SITE_03, "", "Acme Status"),
|
|
629
|
+
),
|
|
630
|
+
).toBe("Acme Status");
|
|
631
|
+
expect(
|
|
632
|
+
SubscriberNotificationDeliveryRecord.getStatusPageName(
|
|
633
|
+
page(SITE_03, "Site 03", "Acme Status"),
|
|
634
|
+
),
|
|
635
|
+
).toBe("Site 03");
|
|
636
|
+
expect(
|
|
637
|
+
SubscriberNotificationDeliveryRecord.getStatusPageName(page(SITE_03, "")),
|
|
638
|
+
).toBe("Untitled status page");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("an empty record has nothing to say", () => {
|
|
642
|
+
expect(
|
|
643
|
+
new SubscriberNotificationDeliveryRecord({
|
|
644
|
+
dedupeEmailAndSms: true,
|
|
645
|
+
}).toMarkdown(),
|
|
646
|
+
).toBe("");
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
/*
|
|
651
|
+
* Delivery: each message is awaited and counted sent or failed. A send
|
|
652
|
+
* fails when it throws, when it answers with an HTTPErrorResponse (the
|
|
653
|
+
* Notification service's email and SMS endpoints, and the Slack and webhook
|
|
654
|
+
* senders, return one rather than throw), when it throws the response itself
|
|
655
|
+
* (the Microsoft Teams sender does), or when it does not answer in time.
|
|
656
|
+
*/
|
|
657
|
+
describe("SubscriberNotificationDeliveryRecord.deliver", () => {
|
|
658
|
+
afterEach(() => {
|
|
659
|
+
jest.restoreAllMocks();
|
|
660
|
+
jest.useRealTimers();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
function quietLogger(): SpyInstance<typeof logger.error> {
|
|
664
|
+
return jest.spyOn(logger, "error").mockImplementation((): void => {});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const failures: Array<[string, () => Promise<unknown>]> = [
|
|
668
|
+
[
|
|
669
|
+
"throws an error",
|
|
670
|
+
(): Promise<unknown> => {
|
|
671
|
+
return Promise.reject(new Error("connect ECONNREFUSED"));
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"answers with an HTTPErrorResponse",
|
|
676
|
+
(): Promise<unknown> => {
|
|
677
|
+
return Promise.resolve(
|
|
678
|
+
new HTTPErrorResponse(500, { message: "SMTP rejected" }, {}),
|
|
679
|
+
);
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
[
|
|
683
|
+
"throws an HTTPErrorResponse",
|
|
684
|
+
(): Promise<unknown> => {
|
|
685
|
+
return Promise.reject(
|
|
686
|
+
new HTTPErrorResponse(400, { message: "Bad Request" }, {}),
|
|
687
|
+
);
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
[
|
|
691
|
+
"rejects with nothing at all",
|
|
692
|
+
(): Promise<unknown> => {
|
|
693
|
+
return Promise.reject(null);
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
[
|
|
697
|
+
"throws before it starts",
|
|
698
|
+
(): Promise<unknown> => {
|
|
699
|
+
throw new Error("Slack Webhook URL must start with https://");
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
for (const [label, send] of failures) {
|
|
705
|
+
test(`a send that ${label} is counted failed, and logged as the subscriber's side of the wire`, async () => {
|
|
706
|
+
const error: SpyInstance<typeof logger.error> = quietLogger();
|
|
707
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
708
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
709
|
+
|
|
710
|
+
record.startStatusPage(site03);
|
|
711
|
+
|
|
712
|
+
await expect(
|
|
713
|
+
record.deliver({
|
|
714
|
+
statusPage: site03,
|
|
715
|
+
method: StatusPageSubscriberNotificationMethod.Webhook,
|
|
716
|
+
send: send,
|
|
717
|
+
logAttributes: { projectId: "p1" },
|
|
718
|
+
}),
|
|
719
|
+
).resolves.toBe(false);
|
|
720
|
+
|
|
721
|
+
expect(record.getFailedCount(SITE_03)).toBe(1);
|
|
722
|
+
expect(
|
|
723
|
+
record.getFailedCount(
|
|
724
|
+
SITE_03,
|
|
725
|
+
StatusPageSubscriberNotificationMethod.Webhook,
|
|
726
|
+
),
|
|
727
|
+
).toBe(1);
|
|
728
|
+
expect(record.getSentCount(SITE_03)).toBe(0);
|
|
729
|
+
expect(record.hasFailures()).toBe(true);
|
|
730
|
+
expect(record.hasAttemptedAny()).toBe(true);
|
|
731
|
+
|
|
732
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
733
|
+
const attributes: JSONObject = error.mock.calls[0]![1] as JSONObject;
|
|
734
|
+
expect(attributes["projectId"]).toBe("p1");
|
|
735
|
+
// Tagged a user error: a subscriber's dead endpoint is not our defect.
|
|
736
|
+
expect(Object.keys(attributes).length).toBeGreaterThan(1);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
test("a send that answers is counted sent, whatever it answers with", async () => {
|
|
741
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
742
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
743
|
+
|
|
744
|
+
record.startStatusPage(site03);
|
|
745
|
+
|
|
746
|
+
for (const answer of [
|
|
747
|
+
undefined,
|
|
748
|
+
new HTTPResponse(200, {}, {}),
|
|
749
|
+
new HTTPResponse(202, { queued: true }, {}),
|
|
750
|
+
]) {
|
|
751
|
+
await expect(
|
|
752
|
+
record.deliver({
|
|
753
|
+
statusPage: site03,
|
|
754
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
755
|
+
send: (): Promise<unknown> => {
|
|
756
|
+
return Promise.resolve(answer);
|
|
757
|
+
},
|
|
758
|
+
}),
|
|
759
|
+
).resolves.toBe(true);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
expect(record.getSentCount(SITE_03)).toBe(3);
|
|
763
|
+
expect(record.getFailedCount(SITE_03)).toBe(0);
|
|
764
|
+
expect(record.hasFailures()).toBe(false);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("a send that does not answer in time is counted failed, and stops being waited for", async () => {
|
|
768
|
+
quietLogger();
|
|
769
|
+
jest.useFakeTimers();
|
|
770
|
+
|
|
771
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
772
|
+
new SubscriberNotificationDeliveryRecord({
|
|
773
|
+
dedupeEmailAndSms: false,
|
|
774
|
+
sendTimeoutInMs: 1000,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
record.startStatusPage(site03);
|
|
778
|
+
|
|
779
|
+
const delivered: Promise<boolean> = record.deliver({
|
|
780
|
+
statusPage: site03,
|
|
781
|
+
method: StatusPageSubscriberNotificationMethod.Webhook,
|
|
782
|
+
send: (): Promise<unknown> => {
|
|
783
|
+
return new Promise(() => {});
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
jest.advanceTimersByTime(1000);
|
|
788
|
+
|
|
789
|
+
await expect(delivered).resolves.toBe(false);
|
|
790
|
+
expect(record.getFailedCount(SITE_03)).toBe(1);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test("waits the send timeout by default: longer than any sender's own retries", () => {
|
|
794
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
795
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
796
|
+
|
|
797
|
+
expect(
|
|
798
|
+
(record as unknown as { sendTimeoutInMs: number }).sendTimeoutInMs,
|
|
799
|
+
).toBe(SubscriberNotificationTiming.SEND_TIMEOUT_IN_MS);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("an email's subject is recorded whether it was sent or failed", async () => {
|
|
803
|
+
quietLogger();
|
|
804
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
805
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
806
|
+
|
|
807
|
+
record.startStatusPage(site03);
|
|
808
|
+
await record.deliver({
|
|
809
|
+
statusPage: site03,
|
|
810
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
811
|
+
subject: "[Incident] Checkout down",
|
|
812
|
+
send: (): Promise<unknown> => {
|
|
813
|
+
return Promise.reject(new Error("no"));
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
expect(record.getSubject(SITE_03)).toBe("[Incident] Checkout down");
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
describe("SubscriberNotificationDeliveryRecord pages sent in full", () => {
|
|
822
|
+
function recordWith(): SubscriberNotificationDeliveryRecord {
|
|
823
|
+
return new SubscriberNotificationDeliveryRecord({
|
|
824
|
+
dedupeEmailAndSms: false,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
test("a page whose every subscriber was reached, with nothing failed, succeeded", () => {
|
|
829
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
830
|
+
|
|
831
|
+
record.startStatusPage(site03);
|
|
832
|
+
record.recordSent({
|
|
833
|
+
statusPage: site03,
|
|
834
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
835
|
+
});
|
|
836
|
+
record.finishStatusPage(site03);
|
|
837
|
+
|
|
838
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(true);
|
|
839
|
+
expect(record.hasFailures()).toBe(false);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("a page with no matching subscriber that was read in full succeeded: nobody is owed it", () => {
|
|
843
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
844
|
+
|
|
845
|
+
record.startStatusPage(site03);
|
|
846
|
+
record.finishStatusPage(site03);
|
|
847
|
+
|
|
848
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(true);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("one failed message means the page was not sent in full", () => {
|
|
852
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
853
|
+
|
|
854
|
+
record.startStatusPage(site03);
|
|
855
|
+
record.recordSent({
|
|
856
|
+
statusPage: site03,
|
|
857
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
858
|
+
});
|
|
859
|
+
record.recordFailed({
|
|
860
|
+
statusPage: site03,
|
|
861
|
+
method: StatusPageSubscriberNotificationMethod.Webhook,
|
|
862
|
+
});
|
|
863
|
+
record.finishStatusPage(site03);
|
|
864
|
+
|
|
865
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(false);
|
|
866
|
+
expect(record.hasFailures()).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test.each([
|
|
870
|
+
StatusPageDeliverySkipReason.Failed,
|
|
871
|
+
StatusPageDeliverySkipReason.OutOfTime,
|
|
872
|
+
])(
|
|
873
|
+
"a page the send stopped part-way through (%s) was not sent in full",
|
|
874
|
+
(reason: StatusPageDeliverySkipReason) => {
|
|
875
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
876
|
+
|
|
877
|
+
record.startStatusPage(site03);
|
|
878
|
+
record.recordSent({
|
|
879
|
+
statusPage: site03,
|
|
880
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
881
|
+
});
|
|
882
|
+
record.skipStatusPage(site03, reason);
|
|
883
|
+
|
|
884
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(false);
|
|
885
|
+
expect(record.hasFailures()).toBe(true);
|
|
886
|
+
},
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
test("a page not read to the end did not succeed", () => {
|
|
890
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
891
|
+
|
|
892
|
+
record.startStatusPage(site03);
|
|
893
|
+
record.recordSent({
|
|
894
|
+
statusPage: site03,
|
|
895
|
+
method: StatusPageSubscriberNotificationMethod.Email,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(false);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test.each([
|
|
902
|
+
StatusPageDeliverySkipReason.HidesIncidents,
|
|
903
|
+
StatusPageDeliverySkipReason.HidesEpisodes,
|
|
904
|
+
StatusPageDeliverySkipReason.AlreadyNotified,
|
|
905
|
+
])(
|
|
906
|
+
"a page passed over (%s) neither succeeded nor counts as a shortfall",
|
|
907
|
+
(reason: StatusPageDeliverySkipReason) => {
|
|
908
|
+
const record: SubscriberNotificationDeliveryRecord = recordWith();
|
|
909
|
+
|
|
910
|
+
record.skipStatusPage(site03, reason);
|
|
911
|
+
|
|
912
|
+
expect(record.didStatusPageSucceed(SITE_03)).toBe(false);
|
|
913
|
+
expect(record.hasFailures()).toBe(false);
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
test("a page never started is unknown to the record", () => {
|
|
918
|
+
expect(recordWith().didStatusPageSucceed(SITE_09)).toBe(false);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
describe("SubscriberNotificationDeliveryRecord sent and failed, in the feed and the status message", () => {
|
|
923
|
+
function sent(
|
|
924
|
+
record: SubscriberNotificationDeliveryRecord,
|
|
925
|
+
statusPage: StatusPage,
|
|
926
|
+
method: StatusPageSubscriberNotificationMethod,
|
|
927
|
+
count: number,
|
|
928
|
+
): void {
|
|
929
|
+
for (let i: number = 0; i < count; i++) {
|
|
930
|
+
record.recordSent({ statusPage, method });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function failed(
|
|
935
|
+
record: SubscriberNotificationDeliveryRecord,
|
|
936
|
+
statusPage: StatusPage,
|
|
937
|
+
method: StatusPageSubscriberNotificationMethod,
|
|
938
|
+
count: number,
|
|
939
|
+
): void {
|
|
940
|
+
for (let i: number = 0; i < count; i++) {
|
|
941
|
+
record.recordFailed({ statusPage, method });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Site 03: 41 email sent. Site 07: 16 email sent, 2 failed, 3 webhook sent.
|
|
946
|
+
function partlyFailedSend(): SubscriberNotificationDeliveryRecord {
|
|
947
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
948
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
949
|
+
|
|
950
|
+
record.startStatusPage(site03);
|
|
951
|
+
sent(record, site03, StatusPageSubscriberNotificationMethod.Email, 41);
|
|
952
|
+
record.finishStatusPage(site03);
|
|
953
|
+
|
|
954
|
+
record.startStatusPage(site07);
|
|
955
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.Email, 16);
|
|
956
|
+
failed(record, site07, StatusPageSubscriberNotificationMethod.Email, 2);
|
|
957
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.Webhook, 3);
|
|
958
|
+
record.finishStatusPage(site07);
|
|
959
|
+
|
|
960
|
+
// Passed over: not in the status message, only in the feed.
|
|
961
|
+
record.skipStatusPage(site09, StatusPageDeliverySkipReason.HidesIncidents);
|
|
962
|
+
|
|
963
|
+
return record;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
test("the feed lists each page with what was sent and what failed", () => {
|
|
967
|
+
expect(partlyFailedSend().toMarkdown()).toBe(
|
|
968
|
+
[
|
|
969
|
+
"**Status pages:**",
|
|
970
|
+
"",
|
|
971
|
+
"- **Site 03**: 41 email sent.",
|
|
972
|
+
"- **Site 07**: 16 email, 3 webhook sent; 2 email failed.",
|
|
973
|
+
"- **Site 09**: not sent, this status page does not show incidents.",
|
|
974
|
+
].join("\n"),
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("a page where every message failed says nothing was sent", () => {
|
|
979
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
980
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
981
|
+
|
|
982
|
+
record.startStatusPage(site03);
|
|
983
|
+
failed(record, site03, StatusPageSubscriberNotificationMethod.SMS, 2);
|
|
984
|
+
record.finishStatusPage(site03);
|
|
985
|
+
|
|
986
|
+
expect(record.toMarkdown()).toContain(
|
|
987
|
+
"- **Site 03**: nothing sent; 2 SMS failed.",
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test("the totals add up across pages", () => {
|
|
992
|
+
expect(partlyFailedSend().getTotals()).toEqual({ sent: 60, failed: 2 });
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test("a send that reached everyone: the job's line, then each page's counts", () => {
|
|
996
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
997
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
998
|
+
|
|
999
|
+
record.startStatusPage(site03);
|
|
1000
|
+
sent(record, site03, StatusPageSubscriberNotificationMethod.Email, 41);
|
|
1001
|
+
record.finishStatusPage(site03);
|
|
1002
|
+
record.startStatusPage(site07);
|
|
1003
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.Email, 18);
|
|
1004
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.SMS, 2);
|
|
1005
|
+
record.finishStatusPage(site07);
|
|
1006
|
+
record.skipStatusPage(site09, StatusPageDeliverySkipReason.AlreadyNotified);
|
|
1007
|
+
|
|
1008
|
+
expect(
|
|
1009
|
+
record.toStatusMessage({
|
|
1010
|
+
sentMessage: "Notifications sent successfully to all subscribers.",
|
|
1011
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1012
|
+
}),
|
|
1013
|
+
).toBe(
|
|
1014
|
+
"Notifications sent successfully to all subscribers. Site 03: 41 email sent. Site 07: 18 email, 2 SMS sent.",
|
|
1015
|
+
);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test("a send that fell short says how many failed, each page's counts, and that Retry sends to every page", () => {
|
|
1019
|
+
expect(
|
|
1020
|
+
partlyFailedSend().toStatusMessage({
|
|
1021
|
+
sentMessage: "Notifications sent successfully to all subscribers.",
|
|
1022
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1023
|
+
}),
|
|
1024
|
+
).toBe(
|
|
1025
|
+
"Not every subscriber was sent this notification: 2 of 62 messages failed. Site 03: 41 email sent. Site 07: 16 email, 3 webhook sent; 2 email failed. Retry sends it again to every status page, including the subscribers who already got it.",
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test("for the created notification, Retry names the pages it will send to", () => {
|
|
1030
|
+
expect(
|
|
1031
|
+
partlyFailedSend().toStatusMessage({
|
|
1032
|
+
sentMessage: "Notifications sent successfully to all subscribers.",
|
|
1033
|
+
retryScope: SubscriberNotificationRetryScope.PagesNotYetSent,
|
|
1034
|
+
}),
|
|
1035
|
+
).toBe(
|
|
1036
|
+
"Not every subscriber was sent this notification: 2 of 62 messages failed. Site 03: 41 email sent. Site 07: 16 email, 3 webhook sent; 2 email failed. Retry sends it again only to the status pages that were not sent it in full: Site 07.",
|
|
1037
|
+
);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
test("a send that ran out of time says so, for the pages it stopped on and the ones it never reached", () => {
|
|
1041
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1042
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1043
|
+
|
|
1044
|
+
record.startStatusPage(site03);
|
|
1045
|
+
sent(record, site03, StatusPageSubscriberNotificationMethod.Email, 5);
|
|
1046
|
+
record.finishStatusPage(site03);
|
|
1047
|
+
record.startStatusPage(site07);
|
|
1048
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.Email, 3);
|
|
1049
|
+
record.skipStatusPage(site07, StatusPageDeliverySkipReason.OutOfTime);
|
|
1050
|
+
record.skipStatusPage(site09, StatusPageDeliverySkipReason.OutOfTime);
|
|
1051
|
+
|
|
1052
|
+
expect(
|
|
1053
|
+
record.toStatusMessage({
|
|
1054
|
+
sentMessage: "Sent.",
|
|
1055
|
+
retryScope: SubscriberNotificationRetryScope.PagesNotYetSent,
|
|
1056
|
+
}),
|
|
1057
|
+
).toBe(
|
|
1058
|
+
"Not every subscriber was sent this notification. The send ran out of time before it reached every subscriber. Site 03: 5 email sent. Site 07: 3 email sent, then the send ran out of time. Site 09: not sent, the send ran out of time before it reached this status page. Retry sends it again only to the status pages that were not sent it in full: Site 07 and Site 09.",
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
expect(record.toMarkdown()).toBe(
|
|
1062
|
+
[
|
|
1063
|
+
"**Status pages:**",
|
|
1064
|
+
"",
|
|
1065
|
+
"- **Site 03**: 5 email sent.",
|
|
1066
|
+
"- **Site 07**: 3 email sent. The send ran out of time part-way through this status page, so some of its subscribers were not sent it.",
|
|
1067
|
+
"- **Site 09**: not sent, the send ran out of time before it reached this status page.",
|
|
1068
|
+
].join("\n"),
|
|
1069
|
+
);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test("a page that failed part-way says so in the status message", () => {
|
|
1073
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1074
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1075
|
+
|
|
1076
|
+
record.startStatusPage(site03);
|
|
1077
|
+
sent(record, site03, StatusPageSubscriberNotificationMethod.Email, 2);
|
|
1078
|
+
record.skipStatusPage(site03, StatusPageDeliverySkipReason.Failed);
|
|
1079
|
+
|
|
1080
|
+
expect(
|
|
1081
|
+
record.toStatusMessage({
|
|
1082
|
+
sentMessage: "Sent.",
|
|
1083
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1084
|
+
}),
|
|
1085
|
+
).toBe(
|
|
1086
|
+
"Not every subscriber was sent this notification. Sending to a status page failed part-way. Site 03: 2 email sent, then sending failed part-way. Retry sends it again to every status page, including the subscribers who already got it.",
|
|
1087
|
+
);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("a long list of pages is cut short in the status message", () => {
|
|
1091
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1092
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1093
|
+
|
|
1094
|
+
for (let i: number = 1; i <= 30; i++) {
|
|
1095
|
+
const site: StatusPage = page(
|
|
1096
|
+
`b0000000-0000-4000-8000-0000000002${i.toString().padStart(2, "0")}`,
|
|
1097
|
+
`Site ${i}`,
|
|
1098
|
+
);
|
|
1099
|
+
record.startStatusPage(site);
|
|
1100
|
+
sent(record, site, StatusPageSubscriberNotificationMethod.Email, 1);
|
|
1101
|
+
record.finishStatusPage(site);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const message: string = record.toStatusMessage({
|
|
1105
|
+
sentMessage: "Sent.",
|
|
1106
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
expect(message).toContain("Site 25: 1 email sent.");
|
|
1110
|
+
expect(message).not.toContain("Site 26:");
|
|
1111
|
+
expect(message).toContain("And 5 more status pages.");
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test("the status message is plain text on one line, page names as written", () => {
|
|
1115
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1116
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1117
|
+
const tricky: StatusPage = page(SITE_03, "*Internal*\nops");
|
|
1118
|
+
|
|
1119
|
+
record.startStatusPage(tricky);
|
|
1120
|
+
sent(record, tricky, StatusPageSubscriberNotificationMethod.Email, 1);
|
|
1121
|
+
record.finishStatusPage(tricky);
|
|
1122
|
+
|
|
1123
|
+
expect(
|
|
1124
|
+
record.toStatusMessage({
|
|
1125
|
+
sentMessage: "Sent.",
|
|
1126
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1127
|
+
}),
|
|
1128
|
+
).toBe("Sent. *Internal* ops: 1 email sent.");
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
test("pages passed over are left out of the message of a send that went to a page", () => {
|
|
1132
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1133
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1134
|
+
|
|
1135
|
+
record.skipStatusPage(site03, StatusPageDeliverySkipReason.HidesIncidents);
|
|
1136
|
+
record.startStatusPage(site07);
|
|
1137
|
+
sent(record, site07, StatusPageSubscriberNotificationMethod.Email, 1);
|
|
1138
|
+
record.finishStatusPage(site07);
|
|
1139
|
+
|
|
1140
|
+
expect(
|
|
1141
|
+
record.toStatusMessage({
|
|
1142
|
+
sentMessage: "Sent.",
|
|
1143
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1144
|
+
}),
|
|
1145
|
+
).toBe("Sent. Site 07: 1 email sent.");
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
/*
|
|
1149
|
+
* A send that went to no status page reached nobody. Its status message
|
|
1150
|
+
* must say so, and why, rather than the job's "sent successfully to all
|
|
1151
|
+
* subscribers": for an incident created on a monitor that only pages
|
|
1152
|
+
* showing scoped incidents list, that is the usual outcome.
|
|
1153
|
+
*/
|
|
1154
|
+
test("a send every page was left out of by scope says nobody was sent it, and why", () => {
|
|
1155
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1156
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: true });
|
|
1157
|
+
|
|
1158
|
+
record.addExcludedStatusPages([
|
|
1159
|
+
{
|
|
1160
|
+
statusPage: site03,
|
|
1161
|
+
reason: StatusPageExclusionReason.OnlyShowsScopedIncidents,
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
statusPage: site07,
|
|
1165
|
+
reason: StatusPageExclusionReason.OnlyShowsScopedIncidents,
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
statusPage: site09,
|
|
1169
|
+
reason: StatusPageExclusionReason.OutsideIncidentScope,
|
|
1170
|
+
},
|
|
1171
|
+
]);
|
|
1172
|
+
|
|
1173
|
+
expect(record.hasFailures()).toBe(false);
|
|
1174
|
+
expect(
|
|
1175
|
+
record.toStatusMessage({
|
|
1176
|
+
sentMessage: "Notifications sent successfully to all subscribers.",
|
|
1177
|
+
retryScope: SubscriberNotificationRetryScope.PagesNotYetSent,
|
|
1178
|
+
}),
|
|
1179
|
+
).toBe(
|
|
1180
|
+
"Not sent to any subscriber: no status page was sent this notification. Left out by its status page scope: 1 status page outside the status pages this is limited to; 2 status pages that only show incidents limited to them.",
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
test("a send whose every page was passed over names each page and why", () => {
|
|
1185
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1186
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1187
|
+
|
|
1188
|
+
record.skipStatusPage(site03, StatusPageDeliverySkipReason.HidesIncidents);
|
|
1189
|
+
record.skipStatusPage(site07, StatusPageDeliverySkipReason.AlreadyNotified);
|
|
1190
|
+
|
|
1191
|
+
expect(
|
|
1192
|
+
record.toStatusMessage({
|
|
1193
|
+
sentMessage: "Sent.",
|
|
1194
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1195
|
+
}),
|
|
1196
|
+
).toBe(
|
|
1197
|
+
"Not sent to any subscriber: no status page was sent this notification. Site 03: not sent, this status page does not show incidents. Site 07: not sent again, its subscribers were already sent this notification.",
|
|
1198
|
+
);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("a send with no status page at all says so", () => {
|
|
1202
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1203
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1204
|
+
|
|
1205
|
+
expect(
|
|
1206
|
+
record.toStatusMessage({
|
|
1207
|
+
sentMessage: "Sent.",
|
|
1208
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1209
|
+
}),
|
|
1210
|
+
).toBe(
|
|
1211
|
+
"Not sent to any subscriber: no status page was sent this notification. No status page lists what it is about.",
|
|
1212
|
+
);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
test("a page the send went to, with no subscriber matched, still leads with the job's line", () => {
|
|
1216
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1217
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1218
|
+
|
|
1219
|
+
record.startStatusPage(site03);
|
|
1220
|
+
record.finishStatusPage(site03);
|
|
1221
|
+
|
|
1222
|
+
expect(
|
|
1223
|
+
record.toStatusMessage({
|
|
1224
|
+
sentMessage: "Sent.",
|
|
1225
|
+
retryScope: SubscriberNotificationRetryScope.EveryPage,
|
|
1226
|
+
}),
|
|
1227
|
+
).toBe("Sent. Site 03: nothing sent, no subscriber matched.");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test("records whether any subscriber's preferences let the send through", () => {
|
|
1231
|
+
const record: SubscriberNotificationDeliveryRecord =
|
|
1232
|
+
new SubscriberNotificationDeliveryRecord({ dedupeEmailAndSms: false });
|
|
1233
|
+
|
|
1234
|
+
expect(record.hasMatchedAnySubscriber()).toBe(false);
|
|
1235
|
+
record.recordSubscriberMatched();
|
|
1236
|
+
expect(record.hasMatchedAnySubscriber()).toBe(true);
|
|
1237
|
+
});
|
|
1238
|
+
});
|