@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,1348 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* IncidentTemplateVariableBuilder: the values an incident's status page
|
|
5
|
+
* subscriber messages are filled with, in each channel's format - and the
|
|
6
|
+
* incident custom fields that reach subscribers, which is what this pins
|
|
7
|
+
* hardest:
|
|
8
|
+
*
|
|
9
|
+
* - a plain value is plain text everywhere, escaped only where it meets
|
|
10
|
+
* HTML (the email body compile, the default template's plainText=), and
|
|
11
|
+
* never escaped twice;
|
|
12
|
+
* - a Rich text (Markdown) value goes through the email Markdown renderer,
|
|
13
|
+
* which escapes raw HTML, and its images are made public when it goes
|
|
14
|
+
* out;
|
|
15
|
+
* - only fields marked "Include in Subscriber Notifications" reach the
|
|
16
|
+
* default messages and webhooks, in the fields' order; every field is
|
|
17
|
+
* offered to custom templates as {{customFields.<key>}};
|
|
18
|
+
* - Boolean, Date and Date and time values read as words, a day, and the
|
|
19
|
+
* status page's time zones.
|
|
20
|
+
*
|
|
21
|
+
* The Markdown renderer, the resource list and the template compiler are the
|
|
22
|
+
* real ones; only the database reads and the image visibility writes are
|
|
23
|
+
* faked.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
jest.mock("../../../../Server/Services/IncidentCustomFieldService", () => {
|
|
27
|
+
return {
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: { findBy: jest.fn() },
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
jest.mock("../../../../Server/Utils/InlineImageAccessTokenSync", () => {
|
|
34
|
+
return {
|
|
35
|
+
__esModule: true,
|
|
36
|
+
syncIsPublicForMarkdownImages: jest.fn(),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
jest.mock("../../../../Server/Utils/Logger", () => {
|
|
41
|
+
return {
|
|
42
|
+
__esModule: true,
|
|
43
|
+
default: {
|
|
44
|
+
debug: jest.fn(),
|
|
45
|
+
info: jest.fn(),
|
|
46
|
+
warn: jest.fn(),
|
|
47
|
+
error: jest.fn(),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
import Incident from "../../../../Models/DatabaseModels/Incident";
|
|
53
|
+
import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
|
|
54
|
+
import Label from "../../../../Models/DatabaseModels/Label";
|
|
55
|
+
import StatusPage from "../../../../Models/DatabaseModels/StatusPage";
|
|
56
|
+
import StatusPageGroup from "../../../../Models/DatabaseModels/StatusPageGroup";
|
|
57
|
+
import StatusPageResource from "../../../../Models/DatabaseModels/StatusPageResource";
|
|
58
|
+
import IncidentCustomFieldService from "../../../../Server/Services/IncidentCustomFieldService";
|
|
59
|
+
import { syncIsPublicForMarkdownImages } from "../../../../Server/Utils/InlineImageAccessTokenSync";
|
|
60
|
+
import IncidentTemplateVariableBuilder, {
|
|
61
|
+
IncidentCustomFieldEmailRow,
|
|
62
|
+
IncidentStatusPageTemplateVariables,
|
|
63
|
+
IncidentTemplateCustomFieldDefinition,
|
|
64
|
+
IncidentTemplateVariables,
|
|
65
|
+
} from "../../../../Server/Utils/StatusPage/IncidentTemplateVariableBuilder";
|
|
66
|
+
import CustomFieldType from "../../../../Types/CustomField/CustomFieldType";
|
|
67
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
68
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
69
|
+
import SafeHtml from "../../../../Types/SafeHtml";
|
|
70
|
+
import SubscriberNotificationTemplateCompiler from "../../../../Types/StatusPage/SubscriberNotificationTemplateCompiler";
|
|
71
|
+
import Timezone from "../../../../Types/Timezone";
|
|
72
|
+
|
|
73
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
74
|
+
"11111111-1111-4111-8111-111111111111",
|
|
75
|
+
);
|
|
76
|
+
const INCIDENT_ID: ObjectID = new ObjectID(
|
|
77
|
+
"33333333-3333-4333-8333-333333333333",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const STATUS_PAGE_URL: string = "https://status.acme.com";
|
|
81
|
+
const DETAILS_URL: string = `${STATUS_PAGE_URL}/incidents/${INCIDENT_ID.toString()}`;
|
|
82
|
+
|
|
83
|
+
const IMAGE_URL: string =
|
|
84
|
+
"https://oneuptime.acme.com/file/image/access-token/abc123";
|
|
85
|
+
|
|
86
|
+
function mock(fn: unknown): jest.Mock {
|
|
87
|
+
return fn as unknown as jest.Mock;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function field(
|
|
91
|
+
data: IncidentTemplateCustomFieldDefinition,
|
|
92
|
+
): IncidentTemplateCustomFieldDefinition {
|
|
93
|
+
return {
|
|
94
|
+
customFieldType: CustomFieldType.Text,
|
|
95
|
+
includeInSubscriberNotifications: false,
|
|
96
|
+
sortOrder: null,
|
|
97
|
+
...data,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function incident(
|
|
102
|
+
customFields: JSONObject = {},
|
|
103
|
+
labels: Array<string> = [],
|
|
104
|
+
): Incident {
|
|
105
|
+
const row: Incident = new Incident();
|
|
106
|
+
row._id = INCIDENT_ID.toString();
|
|
107
|
+
row.projectId = PROJECT_ID;
|
|
108
|
+
row.title = "Checkout requests failing";
|
|
109
|
+
row.customFields = customFields;
|
|
110
|
+
|
|
111
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
112
|
+
severity.name = "Critical";
|
|
113
|
+
row.incidentSeverity = severity;
|
|
114
|
+
|
|
115
|
+
row.labels = labels.map((name: string): Label => {
|
|
116
|
+
const label: Label = new Label();
|
|
117
|
+
label.name = name;
|
|
118
|
+
return label;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return row;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function page(data: {
|
|
125
|
+
name: string;
|
|
126
|
+
pageTitle?: string;
|
|
127
|
+
showIncidentsOnStatusPage?: boolean;
|
|
128
|
+
subscriberTimezones?: Array<Timezone>;
|
|
129
|
+
}): StatusPage {
|
|
130
|
+
const statusPage: StatusPage = new StatusPage();
|
|
131
|
+
statusPage._id = ObjectID.generate().toString();
|
|
132
|
+
statusPage.name = data.name;
|
|
133
|
+
|
|
134
|
+
if (data.pageTitle) {
|
|
135
|
+
statusPage.pageTitle = data.pageTitle;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
statusPage.showIncidentsOnStatusPage =
|
|
139
|
+
data.showIncidentsOnStatusPage === undefined
|
|
140
|
+
? true
|
|
141
|
+
: data.showIncidentsOnStatusPage;
|
|
142
|
+
|
|
143
|
+
if (data.subscriberTimezones) {
|
|
144
|
+
statusPage.subscriberTimezones = data.subscriberTimezones;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return statusPage;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resource(displayName: string, groupName?: string): StatusPageResource {
|
|
151
|
+
const row: StatusPageResource = new StatusPageResource();
|
|
152
|
+
row._id = ObjectID.generate().toString();
|
|
153
|
+
row.displayName = displayName;
|
|
154
|
+
|
|
155
|
+
if (groupName) {
|
|
156
|
+
row.statusPageGroupId = ObjectID.generate();
|
|
157
|
+
const group: StatusPageGroup = new StatusPageGroup();
|
|
158
|
+
group.name = groupName;
|
|
159
|
+
row.statusPageGroup = group;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const SITE_03: StatusPage = page({
|
|
166
|
+
name: "site-03",
|
|
167
|
+
pageTitle: "Site 03 Status",
|
|
168
|
+
subscriberTimezones: [Timezone.UTC, Timezone.AsiaKolkata],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
async function build(data: {
|
|
172
|
+
definitions: Array<IncidentTemplateCustomFieldDefinition>;
|
|
173
|
+
customFields?: JSONObject;
|
|
174
|
+
labels?: Array<string>;
|
|
175
|
+
statusPages?: Array<StatusPage>;
|
|
176
|
+
markdownVariables?: Record<string, string>;
|
|
177
|
+
textVariables?: Record<string, string>;
|
|
178
|
+
}): Promise<IncidentTemplateVariables> {
|
|
179
|
+
return IncidentTemplateVariableBuilder.build({
|
|
180
|
+
incident: incident(data.customFields || {}, data.labels || []),
|
|
181
|
+
statusPages: data.statusPages || [SITE_03],
|
|
182
|
+
markdownVariables: data.markdownVariables,
|
|
183
|
+
textVariables: data.textVariables,
|
|
184
|
+
customFieldDefinitions: data.definitions,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function forSite03(
|
|
189
|
+
variables: IncidentTemplateVariables,
|
|
190
|
+
resources: Array<StatusPageResource> = [resource("Checkout API")],
|
|
191
|
+
): IncidentStatusPageTemplateVariables {
|
|
192
|
+
return variables.forStatusPage({
|
|
193
|
+
statusPage: SITE_03,
|
|
194
|
+
statusPageUrl: STATUS_PAGE_URL,
|
|
195
|
+
detailsUrl: DETAILS_URL,
|
|
196
|
+
resources: resources,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function htmlOf(value: string | SafeHtml | undefined): string {
|
|
201
|
+
expect(SafeHtml.isSafeHtml(value)).toBe(true);
|
|
202
|
+
return (value as SafeHtml).toHtml();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rowTitles(rows: Array<IncidentCustomFieldEmailRow>): Array<string> {
|
|
206
|
+
return rows.map((row: IncidentCustomFieldEmailRow): string => {
|
|
207
|
+
return row.title;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
jest.clearAllMocks();
|
|
213
|
+
mock(syncIsPublicForMarkdownImages).mockResolvedValue(undefined as never);
|
|
214
|
+
mock(IncidentCustomFieldService.findBy).mockResolvedValue([] as never);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("IncidentTemplateVariableBuilder escaping", () => {
|
|
218
|
+
const SCRIPT_VALUE: string = "<script>alert('site')</script> Site 03 & 07";
|
|
219
|
+
const SCRIPT_VALUE_HTML: string =
|
|
220
|
+
"<script>alert('site')</script> Site 03 & 07";
|
|
221
|
+
|
|
222
|
+
test("'<script>' in a Text value is escaped in a custom email body, and nowhere else", async () => {
|
|
223
|
+
const variables: IncidentTemplateVariables = await build({
|
|
224
|
+
definitions: [
|
|
225
|
+
field({
|
|
226
|
+
name: "Affected Location",
|
|
227
|
+
variableKey: "affected_location",
|
|
228
|
+
includeInSubscriberNotifications: true,
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
customFields: { "Affected Location": SCRIPT_VALUE },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
235
|
+
|
|
236
|
+
// A plain string in the email body bag: the compile escapes it, once.
|
|
237
|
+
expect(site.emailBody["customFields.affected_location"]).toBe(SCRIPT_VALUE);
|
|
238
|
+
expect(
|
|
239
|
+
SubscriberNotificationTemplateCompiler.compileEmailBodyTemplate(
|
|
240
|
+
"<p>{{customFields.affected_location}}</p>",
|
|
241
|
+
site.emailBody,
|
|
242
|
+
),
|
|
243
|
+
).toBe(`<p>${SCRIPT_VALUE_HTML}</p>`);
|
|
244
|
+
|
|
245
|
+
// Text channels get it as written.
|
|
246
|
+
expect(site.plainText["customFields.affected_location"]).toBe(SCRIPT_VALUE);
|
|
247
|
+
expect(site.markdown["customFields.affected_location"]).toBe(SCRIPT_VALUE);
|
|
248
|
+
expect(
|
|
249
|
+
SubscriberNotificationTemplateCompiler.compileTemplate(
|
|
250
|
+
"SMS: {{customFields.affected_location}}",
|
|
251
|
+
site.plainText,
|
|
252
|
+
),
|
|
253
|
+
).toBe(`SMS: ${SCRIPT_VALUE}`);
|
|
254
|
+
|
|
255
|
+
// The default email escapes plainText= itself, so it is handed over as written.
|
|
256
|
+
expect(site.customFieldRows).toEqual([
|
|
257
|
+
{ title: "Affected Location", plainText: SCRIPT_VALUE },
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("raw HTML inside a Rich text value is escaped by the email Markdown renderer", async () => {
|
|
262
|
+
const markdown: string =
|
|
263
|
+
'Impact: **EU** <img src=x onerror="alert(1)"> <script>steal()</script> [Reset password](javascript:alert(1)) [status](https://status.acme.com)';
|
|
264
|
+
|
|
265
|
+
const variables: IncidentTemplateVariables = await build({
|
|
266
|
+
definitions: [
|
|
267
|
+
field({
|
|
268
|
+
name: "Impact",
|
|
269
|
+
variableKey: "impact",
|
|
270
|
+
customFieldType: CustomFieldType.Markdown,
|
|
271
|
+
includeInSubscriberNotifications: true,
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
customFields: { Impact: markdown },
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
278
|
+
const html: string = htmlOf(site.emailBody["customFields.impact"]);
|
|
279
|
+
|
|
280
|
+
expect(html).toContain("<strong>EU</strong>");
|
|
281
|
+
expect(html).not.toContain("<script");
|
|
282
|
+
expect(html).not.toContain("<img src=x");
|
|
283
|
+
expect(html).toContain("<script>");
|
|
284
|
+
expect(html).not.toContain("javascript:");
|
|
285
|
+
expect(html).toContain('<a href="https://status.acme.com">status</a>');
|
|
286
|
+
|
|
287
|
+
// The default email gets the same rendered HTML, in its raw slot.
|
|
288
|
+
expect(site.customFieldRows).toEqual([
|
|
289
|
+
{ title: "Impact", renderedHtml: html },
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
// Text channels: flattened for SMS and subjects, as written for chat.
|
|
293
|
+
expect(site.plainText["customFields.impact"]).not.toContain("**");
|
|
294
|
+
expect(site.plainText["customFields.impact"]).toContain("Impact: EU");
|
|
295
|
+
expect(site.markdown["customFields.impact"]).toBe(markdown);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a Rich text value's HTML is not escaped a second time by the email body compile", async () => {
|
|
299
|
+
const variables: IncidentTemplateVariables = await build({
|
|
300
|
+
definitions: [
|
|
301
|
+
field({
|
|
302
|
+
name: "Impact",
|
|
303
|
+
variableKey: "impact",
|
|
304
|
+
customFieldType: CustomFieldType.Markdown,
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
customFields: { Impact: "Payments & refunds **down**" },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const body: string =
|
|
311
|
+
SubscriberNotificationTemplateCompiler.compileEmailBodyTemplate(
|
|
312
|
+
"<div>{{customFields.impact}}</div>",
|
|
313
|
+
forSite03(variables).emailBody,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(body).toBe(
|
|
317
|
+
"<div><p>Payments & refunds <strong>down</strong></p>\n</div>",
|
|
318
|
+
);
|
|
319
|
+
expect(body).not.toContain("&amp;");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("a Long text value keeps its lines in HTML, each escaped", async () => {
|
|
323
|
+
const variables: IncidentTemplateVariables = await build({
|
|
324
|
+
definitions: [
|
|
325
|
+
field({
|
|
326
|
+
name: "Affected Users",
|
|
327
|
+
variableKey: "affected_users",
|
|
328
|
+
customFieldType: CustomFieldType.LongText,
|
|
329
|
+
includeInSubscriberNotifications: true,
|
|
330
|
+
}),
|
|
331
|
+
],
|
|
332
|
+
customFields: { "Affected Users": "EU <admins>\r\nUS & CA\nAPAC" },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
336
|
+
|
|
337
|
+
expect(htmlOf(site.emailBody["customFields.affected_users"])).toBe(
|
|
338
|
+
"EU <admins><br/>US & CA<br/>APAC",
|
|
339
|
+
);
|
|
340
|
+
expect(site.customFieldRows).toEqual([
|
|
341
|
+
{
|
|
342
|
+
title: "Affected Users",
|
|
343
|
+
renderedHtml: "EU <admins><br/>US & CA<br/>APAC",
|
|
344
|
+
},
|
|
345
|
+
]);
|
|
346
|
+
expect(site.plainText["customFields.affected_users"]).toBe(
|
|
347
|
+
"EU <admins>\r\nUS & CA\nAPAC",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("the only HTML in the email body bag is what the builder produced", async () => {
|
|
352
|
+
const variables: IncidentTemplateVariables = await build({
|
|
353
|
+
definitions: [
|
|
354
|
+
field({ name: "Site", variableKey: "site" }),
|
|
355
|
+
field({
|
|
356
|
+
name: "Impact",
|
|
357
|
+
variableKey: "impact",
|
|
358
|
+
customFieldType: CustomFieldType.Markdown,
|
|
359
|
+
}),
|
|
360
|
+
],
|
|
361
|
+
customFields: { Site: "<b>03</b>", Impact: "x" },
|
|
362
|
+
labels: ["<i>eu</i>"],
|
|
363
|
+
markdownVariables: { incidentDescription: "Card payments fail." },
|
|
364
|
+
textVariables: { incidentState: "<em>Resolved</em>" },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
368
|
+
|
|
369
|
+
const htmlNames: Array<string> = Object.entries(site.emailBody)
|
|
370
|
+
.filter(([, value]: [string, string | SafeHtml]): boolean => {
|
|
371
|
+
return SafeHtml.isSafeHtml(value);
|
|
372
|
+
})
|
|
373
|
+
.map(([name]: [string, string | SafeHtml]): string => {
|
|
374
|
+
return name;
|
|
375
|
+
})
|
|
376
|
+
.sort();
|
|
377
|
+
|
|
378
|
+
expect(htmlNames).toEqual([
|
|
379
|
+
"customFields.impact",
|
|
380
|
+
"incidentDescription",
|
|
381
|
+
"resourcesAffected",
|
|
382
|
+
]);
|
|
383
|
+
expect(site.emailBody["customFields.site"]).toBe("<b>03</b>");
|
|
384
|
+
expect(site.emailBody["incidentLabels"]).toBe("<i>eu</i>");
|
|
385
|
+
expect(site.emailBody["incidentState"]).toBe("<em>Resolved</em>");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("IncidentTemplateVariableBuilder which fields reach subscribers", () => {
|
|
390
|
+
const DEFINITIONS: Array<IncidentTemplateCustomFieldDefinition> = [
|
|
391
|
+
field({
|
|
392
|
+
name: "Affected Location",
|
|
393
|
+
variableKey: "affected_location",
|
|
394
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
395
|
+
includeInSubscriberNotifications: true,
|
|
396
|
+
}),
|
|
397
|
+
field({
|
|
398
|
+
name: "Internal Ticket",
|
|
399
|
+
variableKey: "internal_ticket",
|
|
400
|
+
includeInSubscriberNotifications: false,
|
|
401
|
+
}),
|
|
402
|
+
field({
|
|
403
|
+
name: "Customer Impact",
|
|
404
|
+
variableKey: "customer_impact",
|
|
405
|
+
customFieldType: CustomFieldType.Number,
|
|
406
|
+
includeInSubscriberNotifications: true,
|
|
407
|
+
}),
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const VALUES: JSONObject = {
|
|
411
|
+
"Affected Location": "Site 03",
|
|
412
|
+
"Internal Ticket": "OPS-4411",
|
|
413
|
+
"Customer Impact": 0,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
test("only fields marked Include in Subscriber Notifications reach the default email", async () => {
|
|
417
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
418
|
+
await build({ definitions: DEFINITIONS, customFields: VALUES }),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(site.customFieldRows).toEqual([
|
|
422
|
+
{ title: "Affected Location", plainText: "Site 03" },
|
|
423
|
+
// 0 is a value, not an empty field.
|
|
424
|
+
{ title: "Customer Impact", plainText: "0" },
|
|
425
|
+
]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("and the default Slack and Teams lines", async () => {
|
|
429
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
430
|
+
await build({ definitions: DEFINITIONS, customFields: VALUES }),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
expect(site.customFieldsMarkdownLines).toEqual([
|
|
434
|
+
"**Affected Location:** Site 03",
|
|
435
|
+
"**Customer Impact:** 0",
|
|
436
|
+
]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("and webhooks, by key, with each value as stored", async () => {
|
|
440
|
+
const variables: IncidentTemplateVariables = await build({
|
|
441
|
+
definitions: [
|
|
442
|
+
...DEFINITIONS,
|
|
443
|
+
field({
|
|
444
|
+
name: "Sites",
|
|
445
|
+
variableKey: "sites",
|
|
446
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
447
|
+
includeInSubscriberNotifications: true,
|
|
448
|
+
}),
|
|
449
|
+
field({
|
|
450
|
+
name: "Workaround",
|
|
451
|
+
variableKey: "workaround",
|
|
452
|
+
includeInSubscriberNotifications: true,
|
|
453
|
+
}),
|
|
454
|
+
],
|
|
455
|
+
customFields: { ...VALUES, Sites: ["Site 03", "Site 07"] },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(variables.getWebhookCustomFields()).toEqual({
|
|
459
|
+
affected_location: {
|
|
460
|
+
name: "Affected Location",
|
|
461
|
+
type: CustomFieldType.Dropdown,
|
|
462
|
+
value: "Site 03",
|
|
463
|
+
},
|
|
464
|
+
customer_impact: {
|
|
465
|
+
name: "Customer Impact",
|
|
466
|
+
type: CustomFieldType.Number,
|
|
467
|
+
value: 0,
|
|
468
|
+
},
|
|
469
|
+
sites: {
|
|
470
|
+
name: "Sites",
|
|
471
|
+
type: CustomFieldType.MultiSelectDropdown,
|
|
472
|
+
value: ["Site 03", "Site 07"],
|
|
473
|
+
},
|
|
474
|
+
// An included field with no value is still there, as null.
|
|
475
|
+
workaround: {
|
|
476
|
+
name: "Workaround",
|
|
477
|
+
type: CustomFieldType.Text,
|
|
478
|
+
value: null,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("every field, included or not, is offered to custom templates as customFields.<key>", async () => {
|
|
484
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
485
|
+
await build({ definitions: DEFINITIONS, customFields: VALUES }),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
for (const bag of [site.emailBody, site.plainText, site.markdown]) {
|
|
489
|
+
expect(bag["customFields.affected_location"]).toBe("Site 03");
|
|
490
|
+
expect(bag["customFields.internal_ticket"]).toBe("OPS-4411");
|
|
491
|
+
expect(bag["customFields.customer_impact"]).toBe("0");
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a field with no value is an empty string for templates, and left out of the default messages", async () => {
|
|
496
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
497
|
+
await build({
|
|
498
|
+
definitions: DEFINITIONS,
|
|
499
|
+
customFields: { "Affected Location": "", "Customer Impact": null },
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(site.customFieldRows).toEqual([]);
|
|
504
|
+
expect(site.customFieldsMarkdownLines).toEqual([]);
|
|
505
|
+
expect(site.plainText["customFields.affected_location"]).toBe("");
|
|
506
|
+
expect(site.plainText["customFields.internal_ticket"]).toBe("");
|
|
507
|
+
expect(site.emailBody["customFields.customer_impact"]).toBe("");
|
|
508
|
+
expect(
|
|
509
|
+
SubscriberNotificationTemplateCompiler.compileTemplate(
|
|
510
|
+
"[{{customFields.internal_ticket}}]",
|
|
511
|
+
site.plainText,
|
|
512
|
+
),
|
|
513
|
+
).toBe("[]");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("an empty multi-select is no value", async () => {
|
|
517
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
518
|
+
await build({
|
|
519
|
+
definitions: [
|
|
520
|
+
field({
|
|
521
|
+
name: "Sites",
|
|
522
|
+
variableKey: "sites",
|
|
523
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
524
|
+
includeInSubscriberNotifications: true,
|
|
525
|
+
}),
|
|
526
|
+
],
|
|
527
|
+
customFields: { Sites: [] },
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(site.customFieldRows).toEqual([]);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("an answer of only spaces is no value, not an empty row", async () => {
|
|
535
|
+
const variables: IncidentTemplateVariables = await build({
|
|
536
|
+
definitions: [
|
|
537
|
+
field({
|
|
538
|
+
name: "Impact",
|
|
539
|
+
variableKey: "impact",
|
|
540
|
+
customFieldType: CustomFieldType.Markdown,
|
|
541
|
+
includeInSubscriberNotifications: true,
|
|
542
|
+
}),
|
|
543
|
+
field({
|
|
544
|
+
name: "Site",
|
|
545
|
+
variableKey: "site",
|
|
546
|
+
includeInSubscriberNotifications: true,
|
|
547
|
+
}),
|
|
548
|
+
],
|
|
549
|
+
customFields: { Impact: " \n ", Site: " " },
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
553
|
+
|
|
554
|
+
expect(site.customFieldRows).toEqual([]);
|
|
555
|
+
expect(site.customFieldsMarkdownLines).toEqual([]);
|
|
556
|
+
expect(site.plainText["customFields.site"]).toBe("");
|
|
557
|
+
expect(variables.getWebhookCustomFields()).toEqual({
|
|
558
|
+
impact: { name: "Impact", type: CustomFieldType.Markdown, value: null },
|
|
559
|
+
site: { name: "Site", type: CustomFieldType.Text, value: null },
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("an incident with no custom field values at all still fills every placeholder", async () => {
|
|
564
|
+
const variables: IncidentTemplateVariables =
|
|
565
|
+
await IncidentTemplateVariableBuilder.build({
|
|
566
|
+
incident: (() => {
|
|
567
|
+
const row: Incident = incident();
|
|
568
|
+
delete row.customFields;
|
|
569
|
+
return row;
|
|
570
|
+
})(),
|
|
571
|
+
statusPages: [SITE_03],
|
|
572
|
+
customFieldDefinitions: DEFINITIONS,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
expect(forSite03(variables).plainText).toEqual(
|
|
576
|
+
expect.objectContaining({
|
|
577
|
+
"customFields.affected_location": "",
|
|
578
|
+
"customFields.internal_ticket": "",
|
|
579
|
+
"customFields.customer_impact": "",
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("a field without a usable key reaches the default messages but no placeholder", async () => {
|
|
585
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
586
|
+
await build({
|
|
587
|
+
definitions: [
|
|
588
|
+
field({
|
|
589
|
+
name: "Legacy",
|
|
590
|
+
variableKey: null,
|
|
591
|
+
includeInSubscriberNotifications: true,
|
|
592
|
+
}),
|
|
593
|
+
field({
|
|
594
|
+
name: "Odd",
|
|
595
|
+
variableKey: "Not-A-Key",
|
|
596
|
+
includeInSubscriberNotifications: true,
|
|
597
|
+
}),
|
|
598
|
+
],
|
|
599
|
+
customFields: { Legacy: "kept", Odd: "also kept" },
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
expect(rowTitles(site.customFieldRows)).toEqual(["Legacy", "Odd"]);
|
|
604
|
+
expect(
|
|
605
|
+
Object.keys(site.plainText).filter((name: string): boolean => {
|
|
606
|
+
return name.startsWith("customFields.");
|
|
607
|
+
}),
|
|
608
|
+
).toEqual([]);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("values are read by the field's current name", async () => {
|
|
612
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
613
|
+
await build({
|
|
614
|
+
definitions: [
|
|
615
|
+
field({
|
|
616
|
+
name: "Region",
|
|
617
|
+
variableKey: "location",
|
|
618
|
+
includeInSubscriberNotifications: true,
|
|
619
|
+
}),
|
|
620
|
+
],
|
|
621
|
+
customFields: { Region: "EU", Location: "stale" },
|
|
622
|
+
}),
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
expect(site.plainText["customFields.location"]).toBe("EU");
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe("IncidentTemplateVariableBuilder field order", () => {
|
|
630
|
+
test("fields come in their order, lowest first, then those without one in creation order", async () => {
|
|
631
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
632
|
+
await build({
|
|
633
|
+
definitions: [
|
|
634
|
+
field({ name: "No order A", includeInSubscriberNotifications: true }),
|
|
635
|
+
field({
|
|
636
|
+
name: "Third",
|
|
637
|
+
sortOrder: 30,
|
|
638
|
+
includeInSubscriberNotifications: true,
|
|
639
|
+
}),
|
|
640
|
+
field({
|
|
641
|
+
name: "First",
|
|
642
|
+
sortOrder: 1,
|
|
643
|
+
includeInSubscriberNotifications: true,
|
|
644
|
+
}),
|
|
645
|
+
field({ name: "No order B", includeInSubscriberNotifications: true }),
|
|
646
|
+
field({
|
|
647
|
+
name: "Second",
|
|
648
|
+
sortOrder: 2,
|
|
649
|
+
includeInSubscriberNotifications: true,
|
|
650
|
+
}),
|
|
651
|
+
field({
|
|
652
|
+
name: "Also second",
|
|
653
|
+
sortOrder: 2,
|
|
654
|
+
includeInSubscriberNotifications: true,
|
|
655
|
+
}),
|
|
656
|
+
],
|
|
657
|
+
customFields: {
|
|
658
|
+
"No order A": "a",
|
|
659
|
+
Third: "3",
|
|
660
|
+
First: "1",
|
|
661
|
+
"No order B": "b",
|
|
662
|
+
Second: "2",
|
|
663
|
+
"Also second": "2b",
|
|
664
|
+
},
|
|
665
|
+
}),
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
expect(rowTitles(site.customFieldRows)).toEqual([
|
|
669
|
+
"First",
|
|
670
|
+
"Second",
|
|
671
|
+
"Also second",
|
|
672
|
+
"Third",
|
|
673
|
+
"No order A",
|
|
674
|
+
"No order B",
|
|
675
|
+
]);
|
|
676
|
+
expect(site.customFieldsMarkdownLines[0]).toBe("**First:** 1");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("the project's fields are read in creation order, for the order to break ties by", async () => {
|
|
680
|
+
mock(IncidentCustomFieldService.findBy).mockResolvedValue([
|
|
681
|
+
field({
|
|
682
|
+
name: "Later",
|
|
683
|
+
sortOrder: 5,
|
|
684
|
+
includeInSubscriberNotifications: true,
|
|
685
|
+
}),
|
|
686
|
+
field({
|
|
687
|
+
name: "Earlier",
|
|
688
|
+
sortOrder: 1,
|
|
689
|
+
includeInSubscriberNotifications: true,
|
|
690
|
+
}),
|
|
691
|
+
] as never);
|
|
692
|
+
|
|
693
|
+
const variables: IncidentTemplateVariables =
|
|
694
|
+
await IncidentTemplateVariableBuilder.build({
|
|
695
|
+
incident: incident({ Later: "l", Earlier: "e" }),
|
|
696
|
+
statusPages: [SITE_03],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(rowTitles(forSite03(variables).customFieldRows)).toEqual([
|
|
700
|
+
"Earlier",
|
|
701
|
+
"Later",
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
const query: JSONObject = mock(IncidentCustomFieldService.findBy).mock
|
|
705
|
+
.calls[0]![0] as JSONObject;
|
|
706
|
+
|
|
707
|
+
expect(query["query"]).toEqual({ projectId: PROJECT_ID });
|
|
708
|
+
expect(query["sort"]).toEqual({ createdAt: "ASC" });
|
|
709
|
+
expect(query["props"]).toEqual({ isRoot: true });
|
|
710
|
+
expect(query["select"]).toEqual(
|
|
711
|
+
expect.objectContaining({
|
|
712
|
+
name: true,
|
|
713
|
+
variableKey: true,
|
|
714
|
+
customFieldType: true,
|
|
715
|
+
includeInSubscriberNotifications: true,
|
|
716
|
+
sortOrder: true,
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe("IncidentTemplateVariableBuilder value formats", () => {
|
|
723
|
+
test.each([
|
|
724
|
+
[true, "Yes"],
|
|
725
|
+
[false, "No"],
|
|
726
|
+
["true", "Yes"],
|
|
727
|
+
["false", "No"],
|
|
728
|
+
])("a Boolean %p reads as %s", async (value: unknown, text: string) => {
|
|
729
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
730
|
+
await build({
|
|
731
|
+
definitions: [
|
|
732
|
+
field({
|
|
733
|
+
name: "Acknowledged",
|
|
734
|
+
variableKey: "acknowledged",
|
|
735
|
+
customFieldType: CustomFieldType.Boolean,
|
|
736
|
+
includeInSubscriberNotifications: true,
|
|
737
|
+
}),
|
|
738
|
+
],
|
|
739
|
+
customFields: { Acknowledged: value as JSONObject[string] },
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
expect(site.customFieldRows).toEqual([
|
|
744
|
+
{ title: "Acknowledged", plainText: text },
|
|
745
|
+
]);
|
|
746
|
+
expect(site.emailBody["customFields.acknowledged"]).toBe(text);
|
|
747
|
+
expect(site.plainText["customFields.acknowledged"]).toBe(text);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("a Date is the day that was picked, never shifted by a time zone", async () => {
|
|
751
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
752
|
+
await build({
|
|
753
|
+
definitions: [
|
|
754
|
+
field({
|
|
755
|
+
name: "Expected Resolution",
|
|
756
|
+
variableKey: "expected_resolution",
|
|
757
|
+
customFieldType: CustomFieldType.Date,
|
|
758
|
+
includeInSubscriberNotifications: true,
|
|
759
|
+
}),
|
|
760
|
+
],
|
|
761
|
+
customFields: { "Expected Resolution": "2026-09-27T00:00:00.000Z" },
|
|
762
|
+
}),
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
expect(site.customFieldRows).toEqual([
|
|
766
|
+
{ title: "Expected Resolution", plainText: "2026-09-27" },
|
|
767
|
+
]);
|
|
768
|
+
expect(site.markdown["customFields.expected_resolution"]).toBe(
|
|
769
|
+
"2026-09-27",
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
/*
|
|
774
|
+
* The dashboard's date input stores the picked day's midnight in the
|
|
775
|
+
* author's time zone as a UTC instant. Read as its UTC day, 27 Sep picked
|
|
776
|
+
* in Berlin was 26 Sep in every message.
|
|
777
|
+
*/
|
|
778
|
+
test.each([
|
|
779
|
+
["Berlin", "2026-09-26T22:00:00.000Z"],
|
|
780
|
+
["New York", "2026-09-27T04:00:00.000Z"],
|
|
781
|
+
["Auckland", "2026-09-26T11:00:00.000Z"],
|
|
782
|
+
])(
|
|
783
|
+
"a Date picked in %s reads as the day picked on every channel and in the feed",
|
|
784
|
+
async (_where: string, stored: string) => {
|
|
785
|
+
const variables: IncidentTemplateVariables = await build({
|
|
786
|
+
definitions: [
|
|
787
|
+
field({
|
|
788
|
+
name: "Expected Resolution",
|
|
789
|
+
variableKey: "expected_resolution",
|
|
790
|
+
customFieldType: CustomFieldType.Date,
|
|
791
|
+
includeInSubscriberNotifications: true,
|
|
792
|
+
}),
|
|
793
|
+
],
|
|
794
|
+
customFields: { "Expected Resolution": stored },
|
|
795
|
+
});
|
|
796
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
797
|
+
|
|
798
|
+
expect(site.customFieldRows).toEqual([
|
|
799
|
+
{ title: "Expected Resolution", plainText: "2026-09-27" },
|
|
800
|
+
]);
|
|
801
|
+
expect(site.customFieldsMarkdownLines).toEqual([
|
|
802
|
+
"**Expected Resolution:** 2026-09-27",
|
|
803
|
+
]);
|
|
804
|
+
expect(site.emailBody["customFields.expected_resolution"]).toBe(
|
|
805
|
+
"2026-09-27",
|
|
806
|
+
);
|
|
807
|
+
expect(site.plainText["customFields.expected_resolution"]).toBe(
|
|
808
|
+
"2026-09-27",
|
|
809
|
+
);
|
|
810
|
+
expect(site.markdown["customFields.expected_resolution"]).toBe(
|
|
811
|
+
"2026-09-27",
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
await variables.recordIncludedFieldsSent();
|
|
815
|
+
expect(variables.getSentCustomFieldsMarkdown()).toContain(
|
|
816
|
+
"- **Expected Resolution:** 2026\\-09\\-27",
|
|
817
|
+
);
|
|
818
|
+
},
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
test("a Date and time reads in the status page's subscriber time zones", async () => {
|
|
822
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
823
|
+
await build({
|
|
824
|
+
definitions: [
|
|
825
|
+
field({
|
|
826
|
+
name: "Started",
|
|
827
|
+
variableKey: "started",
|
|
828
|
+
customFieldType: CustomFieldType.DateTime,
|
|
829
|
+
includeInSubscriberNotifications: true,
|
|
830
|
+
}),
|
|
831
|
+
],
|
|
832
|
+
customFields: { Started: "2026-09-27T14:05:00.000Z" },
|
|
833
|
+
}),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// One line per zone in HTML, commas in text.
|
|
837
|
+
expect(site.customFieldRows).toEqual([
|
|
838
|
+
{
|
|
839
|
+
title: "Started",
|
|
840
|
+
renderedHtml: "Sep 27 2026, 02:05 PM UTC<br/>Sep 27 2026, 07:35 PM IST",
|
|
841
|
+
},
|
|
842
|
+
]);
|
|
843
|
+
expect(site.plainText["customFields.started"]).toBe(
|
|
844
|
+
"Sep 27 2026, 02:05 PM UTC, Sep 27 2026, 07:35 PM IST",
|
|
845
|
+
);
|
|
846
|
+
expect(htmlOf(site.emailBody["customFields.started"])).toBe(
|
|
847
|
+
"Sep 27 2026, 02:05 PM UTC<br/>Sep 27 2026, 07:35 PM IST",
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("a Date and time on a page with no subscriber time zones uses the usual ones", async () => {
|
|
852
|
+
const variables: IncidentTemplateVariables = await build({
|
|
853
|
+
definitions: [
|
|
854
|
+
field({
|
|
855
|
+
name: "Started",
|
|
856
|
+
variableKey: "started",
|
|
857
|
+
customFieldType: CustomFieldType.DateTime,
|
|
858
|
+
}),
|
|
859
|
+
],
|
|
860
|
+
customFields: { Started: "2026-09-27T14:05:00.000Z" },
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const site: IncidentStatusPageTemplateVariables = variables.forStatusPage({
|
|
864
|
+
statusPage: page({ name: "No zones" }),
|
|
865
|
+
statusPageUrl: STATUS_PAGE_URL,
|
|
866
|
+
detailsUrl: DETAILS_URL,
|
|
867
|
+
resources: [],
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
expect(site.plainText["customFields.started"]).toContain(
|
|
871
|
+
"Sep 27 2026, 02:05 PM UTC",
|
|
872
|
+
);
|
|
873
|
+
expect(site.plainText["customFields.started"]).toContain("AEST");
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("a Date and time that is not a date reads as stored", async () => {
|
|
877
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
878
|
+
await build({
|
|
879
|
+
definitions: [
|
|
880
|
+
field({
|
|
881
|
+
name: "Started",
|
|
882
|
+
variableKey: "started",
|
|
883
|
+
customFieldType: CustomFieldType.DateTime,
|
|
884
|
+
includeInSubscriberNotifications: true,
|
|
885
|
+
}),
|
|
886
|
+
],
|
|
887
|
+
customFields: { Started: "<soon>" },
|
|
888
|
+
}),
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
expect(site.customFieldRows).toEqual([
|
|
892
|
+
{ title: "Started", plainText: "<soon>" },
|
|
893
|
+
]);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("a multi-select lists its options with commas", async () => {
|
|
897
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
898
|
+
await build({
|
|
899
|
+
definitions: [
|
|
900
|
+
field({
|
|
901
|
+
name: "Sites",
|
|
902
|
+
variableKey: "sites",
|
|
903
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
904
|
+
includeInSubscriberNotifications: true,
|
|
905
|
+
}),
|
|
906
|
+
],
|
|
907
|
+
customFields: { Sites: ["Site 03", "", "Site 07"] },
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
expect(site.customFieldRows).toEqual([
|
|
912
|
+
{ title: "Sites", plainText: "Site 03, Site 07" },
|
|
913
|
+
]);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("Rich text and Long text go on their own lines in chat", async () => {
|
|
917
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(
|
|
918
|
+
await build({
|
|
919
|
+
definitions: [
|
|
920
|
+
field({
|
|
921
|
+
name: "Impact",
|
|
922
|
+
customFieldType: CustomFieldType.Markdown,
|
|
923
|
+
includeInSubscriberNotifications: true,
|
|
924
|
+
}),
|
|
925
|
+
field({
|
|
926
|
+
name: "Notes",
|
|
927
|
+
customFieldType: CustomFieldType.LongText,
|
|
928
|
+
includeInSubscriberNotifications: true,
|
|
929
|
+
}),
|
|
930
|
+
],
|
|
931
|
+
customFields: { Impact: "- EU\n- US", Notes: "line 1\nline 2" },
|
|
932
|
+
}),
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expect(site.customFieldsMarkdownLines).toEqual([
|
|
936
|
+
"**Impact:**\n- EU\n- US",
|
|
937
|
+
"**Notes:**\nline 1\nline 2",
|
|
938
|
+
]);
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
describe("IncidentTemplateVariableBuilder shared values", () => {
|
|
943
|
+
test("every bag carries the incident, the page, the event's values and the new incident variables", async () => {
|
|
944
|
+
const variables: IncidentTemplateVariables = await build({
|
|
945
|
+
definitions: [],
|
|
946
|
+
labels: ["payments", "EU", "api"],
|
|
947
|
+
statusPages: [
|
|
948
|
+
SITE_03,
|
|
949
|
+
page({ name: "site-07" }),
|
|
950
|
+
page({ name: "site-09", showIncidentsOnStatusPage: false }),
|
|
951
|
+
],
|
|
952
|
+
markdownVariables: { note: "We are **rolling back**." },
|
|
953
|
+
textVariables: { incidentState: "Identified", postedAt: "Sep 27" },
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables, [
|
|
957
|
+
resource("Checkout API", "Europe"),
|
|
958
|
+
resource("Payments API", "Americas"),
|
|
959
|
+
]);
|
|
960
|
+
|
|
961
|
+
const shared: Record<string, string> = {
|
|
962
|
+
statusPageName: "Site 03 Status",
|
|
963
|
+
statusPageUrl: STATUS_PAGE_URL,
|
|
964
|
+
detailsUrl: DETAILS_URL,
|
|
965
|
+
incidentTitle: "Checkout requests failing",
|
|
966
|
+
incidentSeverity: "Critical",
|
|
967
|
+
// Alphabetical, whatever order the join returned them in.
|
|
968
|
+
incidentLabels: "api, EU, payments",
|
|
969
|
+
// Pages that show the incident, by the name subscribers know them by.
|
|
970
|
+
affectedStatusPages: "Site 03 Status, site-07",
|
|
971
|
+
incidentState: "Identified",
|
|
972
|
+
postedAt: "Sep 27",
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
expect(site.plainText).toEqual({
|
|
976
|
+
...shared,
|
|
977
|
+
resourcesAffected: "Europe: Checkout API; Americas: Payments API",
|
|
978
|
+
note: "We are rolling back.",
|
|
979
|
+
});
|
|
980
|
+
expect(site.markdown).toEqual({
|
|
981
|
+
...shared,
|
|
982
|
+
resourcesAffected: "Europe: Checkout API; Americas: Payments API",
|
|
983
|
+
note: "We are **rolling back**.",
|
|
984
|
+
});
|
|
985
|
+
expect(Object.keys(site.emailBody).sort()).toEqual(
|
|
986
|
+
Object.keys(site.plainText).sort(),
|
|
987
|
+
);
|
|
988
|
+
expect(htmlOf(site.emailBody["resourcesAffected"])).toBe(
|
|
989
|
+
"Europe: Checkout API<br/>Americas: Payments API",
|
|
990
|
+
);
|
|
991
|
+
expect(htmlOf(site.emailBody["note"])).toBe(
|
|
992
|
+
"<p>We are <strong>rolling back</strong>.</p>\n",
|
|
993
|
+
);
|
|
994
|
+
expect(variables.getMarkdownVariable("note").html).toBe(
|
|
995
|
+
"<p>We are <strong>rolling back</strong>.</p>\n",
|
|
996
|
+
);
|
|
997
|
+
expect(site.resourcesAffectedHtml).toBe(
|
|
998
|
+
"Europe: Checkout API<br/>Americas: Payments API",
|
|
999
|
+
);
|
|
1000
|
+
expect(site.resourcesAffectedPlainText).toBe(
|
|
1001
|
+
"Europe: Checkout API; Americas: Payments API",
|
|
1002
|
+
);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
test("a page that lists no resource reads the given text in templates, and nothing in the default messages", async () => {
|
|
1006
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1007
|
+
definitions: [],
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const site: IncidentStatusPageTemplateVariables = variables.forStatusPage({
|
|
1011
|
+
statusPage: SITE_03,
|
|
1012
|
+
statusPageUrl: STATUS_PAGE_URL,
|
|
1013
|
+
detailsUrl: DETAILS_URL,
|
|
1014
|
+
resources: [],
|
|
1015
|
+
noResourcesText: "None",
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
expect(site.plainText["resourcesAffected"]).toBe("None");
|
|
1019
|
+
expect(htmlOf(site.emailBody["resourcesAffected"])).toBe("None");
|
|
1020
|
+
expect(site.resourcesAffectedPlainText).toBe("");
|
|
1021
|
+
expect(site.resourcesAffectedHtml).toBe("");
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
test("an incident with no severity reads ' - '", async () => {
|
|
1025
|
+
const row: Incident = incident();
|
|
1026
|
+
delete row.incidentSeverity;
|
|
1027
|
+
|
|
1028
|
+
const variables: IncidentTemplateVariables =
|
|
1029
|
+
await IncidentTemplateVariableBuilder.build({
|
|
1030
|
+
incident: row,
|
|
1031
|
+
statusPages: [SITE_03],
|
|
1032
|
+
customFieldDefinitions: [],
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
expect(forSite03(variables).plainText["incidentSeverity"]).toBe(" - ");
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test("a missing Markdown value is empty in every form", async () => {
|
|
1039
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1040
|
+
definitions: [],
|
|
1041
|
+
markdownVariables: { incidentDescription: "" },
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const site: IncidentStatusPageTemplateVariables = forSite03(variables);
|
|
1045
|
+
|
|
1046
|
+
expect(site.plainText["incidentDescription"]).toBe("");
|
|
1047
|
+
expect(site.markdown["incidentDescription"]).toBe("");
|
|
1048
|
+
expect(variables.getMarkdownVariable("missing")).toEqual({
|
|
1049
|
+
source: "",
|
|
1050
|
+
html: "",
|
|
1051
|
+
plainText: "",
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
describe("IncidentTemplateVariableBuilder inline images", () => {
|
|
1057
|
+
const RICH_TEXT_WITH_IMAGE: string = `See `;
|
|
1058
|
+
|
|
1059
|
+
function definitions(
|
|
1060
|
+
includeInSubscriberNotifications: boolean,
|
|
1061
|
+
): Array<IncidentTemplateCustomFieldDefinition> {
|
|
1062
|
+
return [
|
|
1063
|
+
field({
|
|
1064
|
+
name: "Impact",
|
|
1065
|
+
variableKey: "impact",
|
|
1066
|
+
customFieldType: CustomFieldType.Markdown,
|
|
1067
|
+
includeInSubscriberNotifications: includeInSubscriberNotifications,
|
|
1068
|
+
}),
|
|
1069
|
+
];
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function publishedMarkdown(): Array<unknown> {
|
|
1073
|
+
return mock(syncIsPublicForMarkdownImages).mock.calls.map(
|
|
1074
|
+
(call: Array<unknown>): Array<unknown> => {
|
|
1075
|
+
return call.slice(0, 2);
|
|
1076
|
+
},
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/*
|
|
1081
|
+
* The regression: build() used to make the included fields' images public
|
|
1082
|
+
* at once, before any page or subscriber was looked at - so an incident
|
|
1083
|
+
* that reached no one still published them.
|
|
1084
|
+
*/
|
|
1085
|
+
test("building the values makes no image public: nothing has gone out", async () => {
|
|
1086
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1087
|
+
definitions: definitions(true),
|
|
1088
|
+
customFields: { Impact: RICH_TEXT_WITH_IMAGE },
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
forSite03(variables);
|
|
1092
|
+
variables.getSentCustomFieldsMarkdown();
|
|
1093
|
+
variables.getWebhookCustomFields();
|
|
1094
|
+
|
|
1095
|
+
expect(syncIsPublicForMarkdownImages).not.toHaveBeenCalled();
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test("an included Rich text field's images are made public when a default message carries it, once", async () => {
|
|
1099
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1100
|
+
definitions: definitions(true),
|
|
1101
|
+
customFields: { Impact: RICH_TEXT_WITH_IMAGE },
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
await variables.recordIncludedFieldsSent();
|
|
1105
|
+
await variables.recordIncludedFieldsSent();
|
|
1106
|
+
|
|
1107
|
+
expect(publishedMarkdown()).toEqual([[RICH_TEXT_WITH_IMAGE, true]]);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
/*
|
|
1111
|
+
* A send awaits several subscribers' messages at once
|
|
1112
|
+
* (SubscriberNotificationFanOut). The second message to carry a field must
|
|
1113
|
+
* wait for the images the first one is making public, not go out while
|
|
1114
|
+
* they are still private - and the images are still made public once.
|
|
1115
|
+
*/
|
|
1116
|
+
test.each([
|
|
1117
|
+
["a default message", "included"],
|
|
1118
|
+
["a custom template", "placed"],
|
|
1119
|
+
])(
|
|
1120
|
+
"messages sent at once through %s all wait for the one sync of its images",
|
|
1121
|
+
async (_label: string, how: string) => {
|
|
1122
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1123
|
+
definitions: definitions(how === "included"),
|
|
1124
|
+
customFields: { Impact: RICH_TEXT_WITH_IMAGE },
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
let finishSync: () => void = (): void => {};
|
|
1128
|
+
mock(syncIsPublicForMarkdownImages).mockImplementation(() => {
|
|
1129
|
+
return new Promise<void>((resolve: () => void) => {
|
|
1130
|
+
finishSync = resolve;
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
const record: () => Promise<void> = (): Promise<void> => {
|
|
1135
|
+
return how === "included"
|
|
1136
|
+
? variables.recordIncludedFieldsSent()
|
|
1137
|
+
: variables.recordFieldsUsedBy(["{{customFields.impact}}"]);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
const done: Array<string> = [];
|
|
1141
|
+
const first: Promise<void> = record().then(() => {
|
|
1142
|
+
done.push("first");
|
|
1143
|
+
});
|
|
1144
|
+
const second: Promise<void> = record().then(() => {
|
|
1145
|
+
done.push("second");
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
await new Promise<void>((resolve: () => void) => {
|
|
1149
|
+
setTimeout(resolve, 0);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Neither message may go out while the images are still private.
|
|
1153
|
+
expect(done).toEqual([]);
|
|
1154
|
+
|
|
1155
|
+
finishSync();
|
|
1156
|
+
await Promise.all([first, second]);
|
|
1157
|
+
|
|
1158
|
+
expect(done.sort()).toEqual(["first", "second"]);
|
|
1159
|
+
expect(publishedMarkdown()).toEqual([[RICH_TEXT_WITH_IMAGE, true]]);
|
|
1160
|
+
},
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
test("a field not included stays private when a default message goes out", async () => {
|
|
1164
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1165
|
+
definitions: definitions(false),
|
|
1166
|
+
customFields: { Impact: RICH_TEXT_WITH_IMAGE },
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
await variables.recordIncludedFieldsSent();
|
|
1170
|
+
await variables.recordFieldsUsedBy([
|
|
1171
|
+
"<p>{{incidentTitle}}</p>",
|
|
1172
|
+
null,
|
|
1173
|
+
undefined,
|
|
1174
|
+
]);
|
|
1175
|
+
|
|
1176
|
+
expect(syncIsPublicForMarkdownImages).not.toHaveBeenCalled();
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
test("a custom template that places the field makes its images public when it is sent, once", async () => {
|
|
1180
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1181
|
+
definitions: definitions(false),
|
|
1182
|
+
customFields: { Impact: RICH_TEXT_WITH_IMAGE },
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
await variables.recordFieldsUsedBy([
|
|
1186
|
+
"<div>{{ customFields.impact }}</div>",
|
|
1187
|
+
]);
|
|
1188
|
+
await variables.recordFieldsUsedBy(["{{customFields.impact}}"]);
|
|
1189
|
+
// The same field again through a default message: already public.
|
|
1190
|
+
await variables.recordIncludedFieldsSent();
|
|
1191
|
+
|
|
1192
|
+
expect(publishedMarkdown()).toEqual([[RICH_TEXT_WITH_IMAGE, true]]);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
test("a placed field with no value is not looked at", async () => {
|
|
1196
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1197
|
+
definitions: definitions(false),
|
|
1198
|
+
customFields: {},
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
await variables.recordFieldsUsedBy(["{{customFields.impact}}"]);
|
|
1202
|
+
|
|
1203
|
+
expect(syncIsPublicForMarkdownImages).not.toHaveBeenCalled();
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("a plain field is never looked at for images", async () => {
|
|
1207
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1208
|
+
definitions: [
|
|
1209
|
+
field({
|
|
1210
|
+
name: "Link",
|
|
1211
|
+
variableKey: "link",
|
|
1212
|
+
includeInSubscriberNotifications: true,
|
|
1213
|
+
}),
|
|
1214
|
+
],
|
|
1215
|
+
customFields: { Link: IMAGE_URL },
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
await variables.recordIncludedFieldsSent();
|
|
1219
|
+
await variables.recordFieldsUsedBy(["{{customFields.link}}"]);
|
|
1220
|
+
|
|
1221
|
+
expect(syncIsPublicForMarkdownImages).not.toHaveBeenCalled();
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
describe("IncidentTemplateVariableBuilder feed record", () => {
|
|
1226
|
+
const DEFINITIONS: Array<IncidentTemplateCustomFieldDefinition> = [
|
|
1227
|
+
field({
|
|
1228
|
+
name: "Affected Location",
|
|
1229
|
+
variableKey: "affected_location",
|
|
1230
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
1231
|
+
includeInSubscriberNotifications: true,
|
|
1232
|
+
sortOrder: 1,
|
|
1233
|
+
}),
|
|
1234
|
+
field({
|
|
1235
|
+
name: "Acknowledged",
|
|
1236
|
+
variableKey: "acknowledged",
|
|
1237
|
+
customFieldType: CustomFieldType.Boolean,
|
|
1238
|
+
includeInSubscriberNotifications: true,
|
|
1239
|
+
sortOrder: 2,
|
|
1240
|
+
}),
|
|
1241
|
+
field({
|
|
1242
|
+
name: "Started",
|
|
1243
|
+
variableKey: "started",
|
|
1244
|
+
customFieldType: CustomFieldType.DateTime,
|
|
1245
|
+
includeInSubscriberNotifications: true,
|
|
1246
|
+
sortOrder: 3,
|
|
1247
|
+
}),
|
|
1248
|
+
field({
|
|
1249
|
+
name: "Impact",
|
|
1250
|
+
variableKey: "impact",
|
|
1251
|
+
customFieldType: CustomFieldType.Markdown,
|
|
1252
|
+
includeInSubscriberNotifications: true,
|
|
1253
|
+
sortOrder: 4,
|
|
1254
|
+
}),
|
|
1255
|
+
field({
|
|
1256
|
+
name: "Internal [ticket]",
|
|
1257
|
+
variableKey: "internal_ticket",
|
|
1258
|
+
sortOrder: 5,
|
|
1259
|
+
}),
|
|
1260
|
+
field({
|
|
1261
|
+
name: "Empty",
|
|
1262
|
+
variableKey: "empty",
|
|
1263
|
+
includeInSubscriberNotifications: true,
|
|
1264
|
+
sortOrder: 6,
|
|
1265
|
+
}),
|
|
1266
|
+
];
|
|
1267
|
+
|
|
1268
|
+
const VALUES: JSONObject = {
|
|
1269
|
+
"Affected Location": "Site 03 *EU*",
|
|
1270
|
+
Acknowledged: false,
|
|
1271
|
+
Started: "2026-09-27T14:05:00.000Z",
|
|
1272
|
+
Impact: "Card payments **fail** in the EU.",
|
|
1273
|
+
"Internal [ticket]": "OPS-4411",
|
|
1274
|
+
Empty: "",
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
test("nothing sent, nothing recorded", async () => {
|
|
1278
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1279
|
+
definitions: DEFINITIONS,
|
|
1280
|
+
customFields: VALUES,
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
expect(variables.getSentCustomFieldsMarkdown()).toBe("");
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
test("a default message records the included fields that hold a value", async () => {
|
|
1287
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1288
|
+
definitions: DEFINITIONS,
|
|
1289
|
+
customFields: VALUES,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
await variables.recordIncludedFieldsSent();
|
|
1293
|
+
|
|
1294
|
+
expect(variables.getSentCustomFieldsMarkdown()).toBe(
|
|
1295
|
+
[
|
|
1296
|
+
"**Custom fields sent:**",
|
|
1297
|
+
"",
|
|
1298
|
+
"- **Affected Location:** Site 03 \\*EU\\*",
|
|
1299
|
+
"- **Acknowledged:** No",
|
|
1300
|
+
"- **Started:** Sep 27 2026, 02:05 PM UTC",
|
|
1301
|
+
"",
|
|
1302
|
+
"**Impact:**",
|
|
1303
|
+
"",
|
|
1304
|
+
"Card payments **fail** in the EU.",
|
|
1305
|
+
].join("\n"),
|
|
1306
|
+
);
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test("a custom template records the fields it placed, included or not", async () => {
|
|
1310
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1311
|
+
definitions: DEFINITIONS,
|
|
1312
|
+
customFields: VALUES,
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
await variables.recordFieldsUsedBy([
|
|
1316
|
+
"Ticket {{customFields.internal_ticket}} at {{customFields.empty}} {{customFields.unknown}}",
|
|
1317
|
+
undefined,
|
|
1318
|
+
]);
|
|
1319
|
+
|
|
1320
|
+
expect(variables.getSentCustomFieldsMarkdown()).toBe(
|
|
1321
|
+
[
|
|
1322
|
+
"**Custom fields sent:**",
|
|
1323
|
+
"",
|
|
1324
|
+
"- **Internal \\[ticket\\]:** OPS\\-4411",
|
|
1325
|
+
].join("\n"),
|
|
1326
|
+
);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test("both together list each field once, in the fields' order", async () => {
|
|
1330
|
+
const variables: IncidentTemplateVariables = await build({
|
|
1331
|
+
definitions: DEFINITIONS,
|
|
1332
|
+
customFields: VALUES,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
await variables.recordFieldsUsedBy(["{{customFields.internal_ticket}}"]);
|
|
1336
|
+
await variables.recordFieldsUsedBy(["{{customFields.affected_location}}"]);
|
|
1337
|
+
await variables.recordIncludedFieldsSent();
|
|
1338
|
+
await variables.recordIncludedFieldsSent();
|
|
1339
|
+
|
|
1340
|
+
const markdown: string = variables.getSentCustomFieldsMarkdown();
|
|
1341
|
+
|
|
1342
|
+
expect(markdown.match(/Affected Location/g)).toHaveLength(1);
|
|
1343
|
+
expect(markdown.indexOf("Affected Location")).toBeLessThan(
|
|
1344
|
+
markdown.indexOf("Internal"),
|
|
1345
|
+
);
|
|
1346
|
+
expect(markdown).not.toContain("Empty");
|
|
1347
|
+
});
|
|
1348
|
+
});
|