@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.
Files changed (440) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/StatusPage.ts +55 -0
  6. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  7. package/Server/API/IncidentAPI.ts +162 -0
  8. package/Server/API/StatusPageAPI.ts +487 -171
  9. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  18. package/Server/Infrastructure/Semaphore.ts +10 -0
  19. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  20. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  21. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  22. package/Server/Services/DatabaseService.ts +93 -10
  23. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  24. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  25. package/Server/Services/IncidentEpisodeService.ts +15 -0
  26. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  27. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  28. package/Server/Services/IncidentService.ts +1535 -65
  29. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  30. package/Server/Services/IncidentTemplateService.ts +109 -0
  31. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  32. package/Server/Services/ScheduledMaintenanceService.ts +68 -18
  33. package/Server/Services/SmsService.ts +10 -0
  34. package/Server/Services/StatusPageService.ts +65 -8
  35. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  36. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  37. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  38. package/Server/Types/Markdown.ts +153 -6
  39. package/Server/Types/Workflow/Components/Email.ts +61 -2
  40. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  41. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  42. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  43. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  44. package/Server/Utils/SSRFProtection.ts +76 -16
  45. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  46. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  47. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  48. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  49. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  50. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  51. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  52. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  53. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  54. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  55. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  56. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  57. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  58. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  59. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  60. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  61. package/Server/Utils/StatusPageResource.ts +51 -20
  62. package/Server/Utils/VM/VMAPI.ts +1 -0
  63. package/Server/Utils/VM/VMRunner.ts +10 -0
  64. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  65. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  66. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  67. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  68. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  69. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  70. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  71. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  72. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  73. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  74. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  75. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +242 -5
  76. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  77. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  78. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  79. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  80. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  81. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  82. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  83. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  84. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  85. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  86. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  87. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  88. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  89. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  90. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  91. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  92. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  93. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  94. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  95. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  96. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  97. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  98. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  99. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  100. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  101. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  102. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  103. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  104. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  105. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  106. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  107. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  108. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  109. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  110. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  111. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  112. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  113. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  114. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  115. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  116. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  117. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  118. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  119. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  120. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  121. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  122. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  123. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  124. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  125. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  126. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  127. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  128. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  129. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  130. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  131. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  132. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  133. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  134. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  135. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  136. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  137. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  138. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  139. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  140. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  141. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  142. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  143. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  144. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  145. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  146. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  147. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  148. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  149. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  150. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  151. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  152. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  153. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  154. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  155. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  156. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  157. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  158. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  159. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  160. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  161. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  162. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  163. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  164. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  165. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  166. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  167. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  168. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  169. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  170. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  171. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  172. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  173. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  174. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  175. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  176. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  177. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  178. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  179. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  180. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  181. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  182. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  183. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  184. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  185. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  186. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  187. package/Tests/Types/SafeHtml.test.ts +102 -0
  188. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  189. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  190. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  191. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  192. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  193. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  194. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  195. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  196. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  197. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  198. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  199. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  200. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  201. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  202. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  203. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  204. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  205. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  206. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  207. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  208. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  209. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  210. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  211. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  212. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  213. package/Types/CustomField/CustomFieldType.ts +14 -0
  214. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  215. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  216. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  217. package/Types/SafeHtml.ts +93 -0
  218. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  219. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  220. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  221. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  222. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  223. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  224. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  225. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  226. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  227. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  228. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  229. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  230. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  231. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  232. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  233. package/UI/Components/Forms/ModelForm.tsx +70 -8
  234. package/UI/Components/Forms/Validation.ts +1 -1
  235. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  236. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  237. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  238. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  239. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  240. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  241. package/Utils/API.ts +247 -2
  242. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  243. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  244. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  245. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  246. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  247. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  248. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  249. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  250. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  251. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  252. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  253. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  254. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  255. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  256. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  257. package/build/dist/Server/API/IncidentAPI.js +100 -1
  258. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  259. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  260. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  261. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  262. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  263. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  264. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  265. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  266. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  267. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  268. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  269. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  270. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  271. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  272. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  273. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  274. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  275. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  276. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  277. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  278. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  279. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  280. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  281. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  282. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  283. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  284. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  285. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  286. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  287. package/build/dist/Server/Services/DatabaseService.js +56 -7
  288. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  289. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  290. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  291. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  292. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  293. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  294. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  295. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  296. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  297. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  298. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  299. package/build/dist/Server/Services/IncidentService.js +1052 -0
  300. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  301. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  302. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  303. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  304. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  305. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  306. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  307. package/build/dist/Server/Services/ScheduledMaintenanceService.js +47 -17
  308. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  309. package/build/dist/Server/Services/SmsService.js +2 -22
  310. package/build/dist/Server/Services/SmsService.js.map +1 -1
  311. package/build/dist/Server/Services/StatusPageService.js +47 -9
  312. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  313. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  314. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  315. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  316. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  317. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  318. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  319. package/build/dist/Server/Types/Markdown.js +110 -6
  320. package/build/dist/Server/Types/Markdown.js.map +1 -1
  321. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  322. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  323. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  324. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  325. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  326. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  327. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  328. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  329. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  330. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  331. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  332. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  333. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  334. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  335. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  336. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  337. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  338. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  339. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  340. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  341. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  342. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  343. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  344. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  345. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  346. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  348. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  349. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  350. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  351. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  352. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  353. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  354. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  355. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  356. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  357. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  358. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  359. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  360. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  361. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  362. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  363. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  364. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  365. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  366. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  367. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  368. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  369. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  370. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  371. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  372. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  373. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  374. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  375. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  376. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  377. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  378. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  379. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  380. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  381. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  382. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  383. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  384. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  385. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  386. package/build/dist/Types/SafeHtml.js +84 -0
  387. package/build/dist/Types/SafeHtml.js.map +1 -0
  388. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  389. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  390. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  391. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  392. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  393. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  394. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  395. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  396. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  397. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  398. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  399. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  400. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  401. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  402. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  403. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  404. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  405. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  406. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  407. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  408. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  409. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  410. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  411. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  412. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  413. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  414. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  415. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  416. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  417. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  418. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  419. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  420. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  421. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  422. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  423. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  424. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  425. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  426. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  427. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  428. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  429. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  430. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  431. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  432. package/build/dist/Utils/API.js +177 -1
  433. package/build/dist/Utils/API.js.map +1 -1
  434. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  435. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  436. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  437. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  438. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  439. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  440. package/package.json +1 -1
@@ -0,0 +1,1168 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+ import type { Mock } from "jest-mock";
10
+
11
+ /*
12
+ * SubscriberNotificationPreviewBuilder: what "Preview notification" and "Send
13
+ * test to me" show and send - for each status page an incident's email would
14
+ * go to, the email its subscribers would get.
15
+ *
16
+ * It runs for real down to the services: the audience builder, the scope
17
+ * helper, the email builder, the values builder, the Markdown renderer and
18
+ * the template compiler are the real ones; only the rows they read are
19
+ * faked. What is pinned:
20
+ *
21
+ * - who may preview what: the audience roles; an incident that exists is
22
+ * read with the caller's own permissions inside the caller's project, so
23
+ * another project's incident and a private one the caller may not read
24
+ * are not found; a draft's monitors, status pages, labels and severity
25
+ * must all be the project's;
26
+ * - nothing will be sent: no monitors, a hidden, private or quiet incident,
27
+ * no page that shows it - and then no email is built at all;
28
+ * - only the pages the caller can read are previewed, each with its "up
29
+ * to" counts and which template is used and why;
30
+ * - no subscriber is ever read (so no address can leave), and building a
31
+ * preview publishes nothing: no custom field image is made public.
32
+ */
33
+
34
+ jest.mock("../../../../Server/Services/IncidentCustomFieldService", () => {
35
+ return {
36
+ __esModule: true,
37
+ default: { findBy: jest.fn() },
38
+ };
39
+ });
40
+
41
+ jest.mock("../../../../Server/Utils/InlineImageAccessTokenSync", () => {
42
+ return {
43
+ __esModule: true,
44
+ syncIsPublicForMarkdownImages: jest.fn(),
45
+ };
46
+ });
47
+
48
+ jest.mock("../../../../Server/Utils/Logger", () => {
49
+ return {
50
+ __esModule: true,
51
+ default: {
52
+ debug: jest.fn(),
53
+ info: jest.fn(),
54
+ warn: jest.fn(),
55
+ error: jest.fn(),
56
+ },
57
+ };
58
+ });
59
+
60
+ import Incident from "../../../../Models/DatabaseModels/Incident";
61
+ import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
62
+ import IncidentState from "../../../../Models/DatabaseModels/IncidentState";
63
+ import Label from "../../../../Models/DatabaseModels/Label";
64
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
65
+ import StatusPage from "../../../../Models/DatabaseModels/StatusPage";
66
+ import StatusPageResource from "../../../../Models/DatabaseModels/StatusPageResource";
67
+ import StatusPageSubscriberNotificationTemplate from "../../../../Models/DatabaseModels/StatusPageSubscriberNotificationTemplate";
68
+ import DatabaseConfig from "../../../../Server/DatabaseConfig";
69
+ import CustomFieldMappingService from "../../../../Server/Services/CustomFieldMappingService";
70
+ import IncidentCustomFieldService from "../../../../Server/Services/IncidentCustomFieldService";
71
+ import IncidentService from "../../../../Server/Services/IncidentService";
72
+ import IncidentSeverityService from "../../../../Server/Services/IncidentSeverityService";
73
+ import LabelService from "../../../../Server/Services/LabelService";
74
+ import MonitorService from "../../../../Server/Services/MonitorService";
75
+ import StatusPageResourceService from "../../../../Server/Services/StatusPageResourceService";
76
+ import StatusPageService from "../../../../Server/Services/StatusPageService";
77
+ import StatusPageSubscriberNotificationTemplateService from "../../../../Server/Services/StatusPageSubscriberNotificationTemplateService";
78
+ import StatusPageSubscriberService from "../../../../Server/Services/StatusPageSubscriberService";
79
+ import { syncIsPublicForMarkdownImages } from "../../../../Server/Utils/InlineImageAccessTokenSync";
80
+ import SubscriberNotificationPreviewBuilder, {
81
+ SubscriberNotificationPreviewBuild,
82
+ SubscriberNotificationPreviewPage,
83
+ } from "../../../../Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder";
84
+ import Hostname from "../../../../Types/API/Hostname";
85
+ import Protocol from "../../../../Types/API/Protocol";
86
+ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
87
+ import CustomFieldType from "../../../../Types/CustomField/CustomFieldType";
88
+ import Dictionary from "../../../../Types/Dictionary";
89
+ import EmailTemplateType from "../../../../Types/Email/EmailTemplateType";
90
+ import BadDataException from "../../../../Types/Exception/BadDataException";
91
+ import NotAuthorizedException from "../../../../Types/Exception/NotAuthorizedException";
92
+ import NotFoundException from "../../../../Types/Exception/NotFoundException";
93
+ import { JSONObject } from "../../../../Types/JSON";
94
+ import ObjectID from "../../../../Types/ObjectID";
95
+ import Permission, {
96
+ UserPermission,
97
+ UserTenantAccessPermission,
98
+ } from "../../../../Types/Permission";
99
+ import IncidentSubscriberAudience, {
100
+ IncidentSubscriberAudienceCounts,
101
+ } from "../../../../Types/StatusPage/IncidentSubscriberAudience";
102
+ import SubscriberNotificationPreview, {
103
+ SubscriberEmailTemplateChoiceReason,
104
+ SubscriberNotificationPreviewEvent,
105
+ SubscriberNotificationPreviewIncidentDraft,
106
+ SubscriberNotificationPreviewNothingSentReason,
107
+ SubscriberNotificationPreviewRequest,
108
+ SubscriberNotificationSendTestRequest,
109
+ } from "../../../../Types/StatusPage/SubscriberNotificationPreview";
110
+
111
+ const PROJECT_ID: ObjectID = new ObjectID(
112
+ "10000000-0000-4000-8000-000000000001",
113
+ );
114
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
115
+ "10000000-0000-4000-8000-000000000002",
116
+ );
117
+ const USER_ID: ObjectID = new ObjectID("20000000-0000-4000-8000-000000000001");
118
+
119
+ const INCIDENT_ID: string = "a0000000-0000-4000-8000-00000000000a";
120
+ const PRIVATE_INCIDENT_ID: string = "a0000000-0000-4000-8000-00000000000b";
121
+ const OTHER_PROJECT_INCIDENT_ID: string =
122
+ "a0000000-0000-4000-8000-00000000000c";
123
+
124
+ const MONITOR_ID: string = "c0000000-0000-4000-8000-000000000001";
125
+ const OTHER_PROJECT_MONITOR_ID: string = "c0000000-0000-4000-8000-000000000009";
126
+
127
+ const SITE_1: string = "b0000000-0000-4000-8000-000000000001";
128
+ const SITE_2: string = "b0000000-0000-4000-8000-000000000002";
129
+ const SITE_3: string = "b0000000-0000-4000-8000-000000000003";
130
+
131
+ const SEVERITY_ID: string = "d0000000-0000-4000-8000-000000000001";
132
+ const OTHER_PROJECT_SEVERITY_ID: string =
133
+ "d0000000-0000-4000-8000-000000000009";
134
+ const LABEL_ID: string = "e0000000-0000-4000-8000-000000000001";
135
+ const OTHER_PROJECT_LABEL_ID: string = "e0000000-0000-4000-8000-000000000009";
136
+
137
+ const IMAGE_URL: string =
138
+ "https://oneuptime.acme.com/file/image/access-token/abc123";
139
+
140
+ interface PageFixture {
141
+ id: string;
142
+ name: string;
143
+ smtp?: boolean | undefined;
144
+ }
145
+
146
+ // Three site pages; each lists the monitor.
147
+ const PAGES: Array<PageFixture> = [
148
+ { id: SITE_1, name: "Site 01", smtp: true },
149
+ { id: SITE_2, name: "Site 02" },
150
+ { id: SITE_3, name: "Site 03" },
151
+ ];
152
+
153
+ const COUNTS: Dictionary<IncidentSubscriberAudienceCounts> = {
154
+ [SITE_1]: { ...IncidentSubscriberAudience.getEmptyCounts(), email: 41 },
155
+ [SITE_2]: { ...IncidentSubscriberAudience.getEmptyCounts(), email: 18 },
156
+ [SITE_3]: { ...IncidentSubscriberAudience.getEmptyCounts(), sms: 2 },
157
+ };
158
+
159
+ // The pages the caller may read; null reads all of them.
160
+ let readablePageIds: Array<string> | null = null;
161
+ // Incidents the caller may read (the rest are private to them).
162
+ let readableIncidentIds: Array<string> = [];
163
+ // The custom email template linked to each page, by id.
164
+ let templates: Dictionary<StatusPageSubscriberNotificationTemplate> = {};
165
+ // The status pages the incident is limited to; null is unscoped.
166
+ let storedScope: Array<string> | null = null;
167
+
168
+ function sameId(a: unknown, b: unknown): boolean {
169
+ return (
170
+ String(a?.toString() || "").toLowerCase() ===
171
+ String(b?.toString() || "").toLowerCase()
172
+ );
173
+ }
174
+
175
+ // The ids QueryHelper.any was given (it builds a Raw IN operator).
176
+ function idsIn(operator: unknown): Array<string> {
177
+ const raw: { objectLiteralParameters?: Dictionary<unknown> } = operator as {
178
+ objectLiteralParameters?: Dictionary<unknown>;
179
+ };
180
+
181
+ return (
182
+ Object.values(raw.objectLiteralParameters || {}) as Array<
183
+ Array<string | ObjectID>
184
+ >
185
+ )
186
+ .flat()
187
+ .map((id: string | ObjectID): string => {
188
+ return id.toString().toLowerCase();
189
+ });
190
+ }
191
+
192
+ function props(
193
+ grants: Array<Permission> = [Permission.ProjectMember],
194
+ ): DatabaseCommonInteractionProps {
195
+ const tenantPermission: UserTenantAccessPermission = {
196
+ _type: "UserTenantAccessPermission",
197
+ projectId: PROJECT_ID,
198
+ permissions: grants.map((permission: Permission): UserPermission => {
199
+ return {
200
+ _type: "UserPermission",
201
+ permission: permission,
202
+ labelIds: [],
203
+ isBlockPermission: false,
204
+ } as UserPermission;
205
+ }),
206
+ } as UserTenantAccessPermission;
207
+
208
+ return {
209
+ userId: USER_ID,
210
+ tenantId: PROJECT_ID,
211
+ userTenantAccessPermission: {
212
+ [PROJECT_ID.toString()]: tenantPermission,
213
+ },
214
+ };
215
+ }
216
+
217
+ function makePage(fixture: PageFixture): StatusPage {
218
+ const page: StatusPage = new StatusPage();
219
+ page._id = fixture.id;
220
+ page.name = fixture.name;
221
+ page.pageTitle = `${fixture.name} Status`;
222
+ page.projectId = PROJECT_ID;
223
+ page.isPublicStatusPage = true;
224
+ page.onlyShowScopedIncidents = false;
225
+ page.showIncidentsOnStatusPage = true;
226
+
227
+ if (fixture.smtp) {
228
+ (page as unknown as JSONObject)["smtpConfig"] = {
229
+ _id: "f0000000-0000-4000-8000-000000000001",
230
+ };
231
+ }
232
+
233
+ return page;
234
+ }
235
+
236
+ function draft(
237
+ overrides: Partial<SubscriberNotificationPreviewIncidentDraft> = {},
238
+ ): SubscriberNotificationPreviewRequest {
239
+ return {
240
+ event: SubscriberNotificationPreviewEvent.IncidentCreated,
241
+ incident: {
242
+ title: "Checkout requests failing",
243
+ description: "Payments fail in **Europe**.",
244
+ incidentSeverityId: SEVERITY_ID,
245
+ monitorIds: [MONITOR_ID],
246
+ statusPageIds: [],
247
+ labelIds: [LABEL_ID],
248
+ customFields: {},
249
+ isPrivate: false,
250
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
251
+ ...overrides,
252
+ },
253
+ };
254
+ }
255
+
256
+ function publicNote(
257
+ incidentId: string = INCIDENT_ID,
258
+ ): SubscriberNotificationPreviewRequest {
259
+ return {
260
+ event: SubscriberNotificationPreviewEvent.IncidentPublicNoteCreated,
261
+ incidentId: incidentId,
262
+ note: "We are **rolling back** the release.",
263
+ postedAt: null,
264
+ };
265
+ }
266
+
267
+ async function preview(
268
+ request: SubscriberNotificationPreviewRequest,
269
+ options: {
270
+ callerProps?: DatabaseCommonInteractionProps;
271
+ onlyStatusPageId?: string;
272
+ } = {},
273
+ ): Promise<SubscriberNotificationPreviewBuild> {
274
+ return SubscriberNotificationPreviewBuilder.build({
275
+ projectId: PROJECT_ID,
276
+ props: options.callerProps || props(),
277
+ request: request,
278
+ onlyStatusPageId: options.onlyStatusPageId,
279
+ });
280
+ }
281
+
282
+ function pageIds(build: SubscriberNotificationPreviewBuild): Array<string> {
283
+ return build.statusPages.map(
284
+ (page: SubscriberNotificationPreviewPage): string => {
285
+ return page.statusPageId;
286
+ },
287
+ );
288
+ }
289
+
290
+ function body(page: SubscriberNotificationPreviewPage): JSONObject {
291
+ return page.email.envelope.vars as JSONObject;
292
+ }
293
+
294
+ let getSubscribersByStatusPage: Mock;
295
+ let subscriberFindBy: Mock;
296
+ let applyMappingsToCreate: Mock;
297
+
298
+ beforeEach(() => {
299
+ readablePageIds = null;
300
+ readableIncidentIds = [INCIDENT_ID];
301
+ storedScope = null;
302
+ templates = {};
303
+
304
+ (syncIsPublicForMarkdownImages as unknown as jest.Mock).mockReset();
305
+
306
+ // One Rich text field, included in subscriber notifications.
307
+ (IncidentCustomFieldService.findBy as unknown as jest.Mock).mockReset();
308
+ (IncidentCustomFieldService.findBy as unknown as jest.Mock).mockResolvedValue(
309
+ [
310
+ {
311
+ name: "Impact details",
312
+ variableKey: "impact_details",
313
+ customFieldType: CustomFieldType.Markdown,
314
+ includeInSubscriberNotifications: true,
315
+ sortOrder: 1,
316
+ },
317
+ {
318
+ name: "Customer count",
319
+ variableKey: "customer_count",
320
+ customFieldType: CustomFieldType.Number,
321
+ includeInSubscriberNotifications: true,
322
+ sortOrder: 2,
323
+ },
324
+ ] as never,
325
+ );
326
+
327
+ jest
328
+ .spyOn(DatabaseConfig, "getHost")
329
+ .mockResolvedValue(Hostname.fromString("oneuptime.acme.com") as never);
330
+ jest
331
+ .spyOn(DatabaseConfig, "getHttpProtocol")
332
+ .mockResolvedValue(Protocol.HTTPS as never);
333
+
334
+ jest.spyOn(StatusPageService, "getStatusPageURL").mockImplementation((async (
335
+ id: ObjectID,
336
+ ): Promise<string> => {
337
+ return `https://status.acme.com/${id.toString()}`;
338
+ }) as never);
339
+
340
+ // Every page lists the monitor.
341
+ jest
342
+ .spyOn(StatusPageResourceService, "findByMonitors")
343
+ .mockImplementation((async (data: {
344
+ monitorIds: Array<ObjectID>;
345
+ }): Promise<Array<StatusPageResource>> => {
346
+ if (
347
+ !data.monitorIds.some((id: ObjectID): boolean => {
348
+ return sameId(id, MONITOR_ID);
349
+ })
350
+ ) {
351
+ return [];
352
+ }
353
+
354
+ return PAGES.map((page: PageFixture, index: number) => {
355
+ const resource: StatusPageResource = new StatusPageResource();
356
+ resource._id = `90000000-0000-4000-8000-00000000000${index}`;
357
+ resource.statusPageId = new ObjectID(page.id);
358
+ resource.displayName = `Checkout on ${page.name}`;
359
+ return resource;
360
+ });
361
+ }) as never);
362
+
363
+ jest
364
+ .spyOn(StatusPageSubscriberService, "getStatusPagesToSendNotification")
365
+ .mockImplementation((async (
366
+ ids: Array<ObjectID>,
367
+ ): Promise<Array<StatusPage>> => {
368
+ return PAGES.filter((page: PageFixture): boolean => {
369
+ return ids.some((id: ObjectID): boolean => {
370
+ return sameId(id, page.id);
371
+ });
372
+ }).map(makePage);
373
+ }) as never);
374
+
375
+ // Root reads check the project; the caller's reads are what they may read.
376
+ jest.spyOn(StatusPageService, "findBy").mockImplementation((async (findBy: {
377
+ query: JSONObject;
378
+ props: DatabaseCommonInteractionProps;
379
+ }): Promise<Array<StatusPage>> => {
380
+ const wanted: Array<string> = idsIn(findBy.query["_id"]);
381
+
382
+ return PAGES.filter((page: PageFixture): boolean => {
383
+ return (
384
+ wanted.includes(page.id) &&
385
+ sameId(PROJECT_ID, findBy.query["projectId"]) &&
386
+ (findBy.props.isRoot ||
387
+ readablePageIds === null ||
388
+ readablePageIds.includes(page.id))
389
+ );
390
+ }).map(makePage);
391
+ }) as never);
392
+
393
+ jest
394
+ .spyOn(StatusPageSubscriberService, "countActiveSubscribersByChannel")
395
+ .mockImplementation((async (data: {
396
+ statusPageIds: Array<ObjectID>;
397
+ }): Promise<Dictionary<IncidentSubscriberAudienceCounts>> => {
398
+ const result: Dictionary<IncidentSubscriberAudienceCounts> = {};
399
+
400
+ for (const id of data.statusPageIds) {
401
+ const key: string = id.toString().toLowerCase();
402
+
403
+ if (COUNTS[key]) {
404
+ result[key] = COUNTS[key]!;
405
+ }
406
+ }
407
+
408
+ return result;
409
+ }) as never);
410
+
411
+ // Neither may ever be reached: they read subscribers out, addresses and all.
412
+ getSubscribersByStatusPage = jest.fn();
413
+ jest
414
+ .spyOn(StatusPageSubscriberService, "getSubscribersByStatusPage")
415
+ .mockImplementation(getSubscribersByStatusPage as never);
416
+ subscriberFindBy = jest.fn();
417
+ jest
418
+ .spyOn(StatusPageSubscriberService, "findBy")
419
+ .mockImplementation(subscriberFindBy as never);
420
+
421
+ jest.spyOn(MonitorService, "findBy").mockImplementation((async (findBy: {
422
+ query: JSONObject;
423
+ }): Promise<Array<Monitor>> => {
424
+ return idsIn(findBy.query["_id"])
425
+ .filter((id: string): boolean => {
426
+ return (
427
+ id === MONITOR_ID && sameId(PROJECT_ID, findBy.query["projectId"])
428
+ );
429
+ })
430
+ .map((id: string): Monitor => {
431
+ const monitor: Monitor = new Monitor();
432
+ monitor._id = id;
433
+ return monitor;
434
+ });
435
+ }) as never);
436
+
437
+ jest
438
+ .spyOn(IncidentSeverityService, "findOneBy")
439
+ .mockImplementation((async (findOneBy: {
440
+ query: JSONObject;
441
+ }): Promise<IncidentSeverity | null> => {
442
+ if (
443
+ !sameId(findOneBy.query["_id"], SEVERITY_ID) ||
444
+ !sameId(findOneBy.query["projectId"], PROJECT_ID)
445
+ ) {
446
+ return null;
447
+ }
448
+
449
+ const severity: IncidentSeverity = new IncidentSeverity();
450
+ severity._id = SEVERITY_ID;
451
+ severity.name = "Major";
452
+ return severity;
453
+ }) as never);
454
+
455
+ jest.spyOn(LabelService, "findBy").mockImplementation((async (findBy: {
456
+ query: JSONObject;
457
+ }): Promise<Array<Label>> => {
458
+ return idsIn(findBy.query["_id"])
459
+ .filter((id: string): boolean => {
460
+ return id === LABEL_ID && sameId(findBy.query["projectId"], PROJECT_ID);
461
+ })
462
+ .map((id: string): Label => {
463
+ const label: Label = new Label();
464
+ label._id = id;
465
+ label.name = "Europe";
466
+ return label;
467
+ });
468
+ }) as never);
469
+
470
+ applyMappingsToCreate = jest.fn(async (): Promise<void> => {
471
+ return undefined;
472
+ });
473
+ jest
474
+ .spyOn(CustomFieldMappingService, "applyMappingsToCreate")
475
+ .mockImplementation(applyMappingsToCreate as never);
476
+
477
+ // The stored scope, as IncidentStatusPageScope reads it.
478
+ jest.spyOn(IncidentService, "findBy").mockImplementation((async (findBy: {
479
+ query: JSONObject;
480
+ }): Promise<Array<Incident>> => {
481
+ return idsIn(findBy.query["_id"]).map((id: string): Incident => {
482
+ const incident: Incident = new Incident();
483
+ incident._id = id;
484
+ incident.isScopedToStatusPages = storedScope !== null;
485
+ incident.statusPages = (storedScope || []).map(
486
+ (pageId: string): StatusPage => {
487
+ const page: StatusPage = new StatusPage();
488
+ page._id = pageId;
489
+ return page;
490
+ },
491
+ );
492
+ return incident;
493
+ });
494
+ }) as never);
495
+
496
+ /*
497
+ * The incidents: one the caller may read, one private to others, and one
498
+ * in another project. The caller's read applies their access; a root read
499
+ * reads any row of the project it names.
500
+ */
501
+ jest
502
+ .spyOn(IncidentService, "findOneBy")
503
+ .mockImplementation((async (findOneBy: {
504
+ query: JSONObject;
505
+ props: DatabaseCommonInteractionProps;
506
+ }): Promise<Incident | null> => {
507
+ const rows: Array<{ id: string; projectId: ObjectID }> = [
508
+ { id: INCIDENT_ID, projectId: PROJECT_ID },
509
+ { id: PRIVATE_INCIDENT_ID, projectId: PROJECT_ID },
510
+ { id: OTHER_PROJECT_INCIDENT_ID, projectId: OTHER_PROJECT_ID },
511
+ ];
512
+
513
+ const row: { id: string; projectId: ObjectID } | undefined = rows.find(
514
+ (candidate: { id: string; projectId: ObjectID }): boolean => {
515
+ return (
516
+ sameId(candidate.id, findOneBy.query["_id"]) &&
517
+ sameId(candidate.projectId, findOneBy.query["projectId"])
518
+ );
519
+ },
520
+ );
521
+
522
+ if (!row) {
523
+ return null;
524
+ }
525
+
526
+ if (!findOneBy.props.isRoot && !readableIncidentIds.includes(row.id)) {
527
+ return null;
528
+ }
529
+
530
+ const incident: Incident = new Incident();
531
+ incident._id = row.id;
532
+ incident.projectId = row.projectId;
533
+ incident.title = "Checkout requests failing";
534
+ incident.description = "Payments fail.";
535
+ incident.isVisibleOnStatusPage = true;
536
+ incident.isPrivate = false;
537
+ incident.customFields = {
538
+ "Impact details": `Errors at checkout. ![graph](${IMAGE_URL})`,
539
+ };
540
+
541
+ const monitor: Monitor = new Monitor();
542
+ monitor._id = MONITOR_ID;
543
+ incident.monitors = [monitor];
544
+
545
+ const severity: IncidentSeverity = new IncidentSeverity();
546
+ severity.name = "Critical";
547
+ incident.incidentSeverity = severity;
548
+
549
+ const state: IncidentState = new IncidentState();
550
+ state.name = "Identified";
551
+ incident.currentIncidentState = state;
552
+
553
+ incident.labels = [];
554
+
555
+ return incident;
556
+ }) as never);
557
+
558
+ jest
559
+ .spyOn(
560
+ StatusPageSubscriberNotificationTemplateService,
561
+ "getTemplateForStatusPage",
562
+ )
563
+ .mockImplementation((async (data: {
564
+ statusPageId: ObjectID;
565
+ }): Promise<StatusPageSubscriberNotificationTemplate | null> => {
566
+ return templates[data.statusPageId.toString().toLowerCase()] || null;
567
+ }) as never);
568
+ });
569
+
570
+ afterEach(() => {
571
+ jest.restoreAllMocks();
572
+ });
573
+
574
+ describe("who may preview what", () => {
575
+ test("a caller with none of the audience roles is refused", async () => {
576
+ await expect(
577
+ preview(draft(), { callerProps: props([Permission.StatusPageViewer]) }),
578
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
579
+ });
580
+
581
+ test("another project's incident is not found", async () => {
582
+ await expect(
583
+ preview(publicNote(OTHER_PROJECT_INCIDENT_ID)),
584
+ ).rejects.toBeInstanceOf(NotFoundException);
585
+ });
586
+
587
+ test("a private incident the caller may not read is not found, and nothing about it is read as root", async () => {
588
+ await expect(
589
+ preview(publicNote(PRIVATE_INCIDENT_ID)),
590
+ ).rejects.toBeInstanceOf(NotFoundException);
591
+
592
+ const rootReads: Array<unknown> = (
593
+ IncidentService.findOneBy as unknown as jest.Mock
594
+ ).mock.calls.filter((call: Array<unknown>): boolean => {
595
+ return (
596
+ (call[0] as { props: DatabaseCommonInteractionProps }).props.isRoot ===
597
+ true
598
+ );
599
+ });
600
+
601
+ expect(rootReads).toEqual([]);
602
+ });
603
+
604
+ test("once the caller may read the incident, it is previewed", async () => {
605
+ readableIncidentIds = [INCIDENT_ID, PRIVATE_INCIDENT_ID];
606
+
607
+ const build: SubscriberNotificationPreviewBuild = await preview(
608
+ publicNote(PRIVATE_INCIDENT_ID),
609
+ );
610
+
611
+ expect(pageIds(build)).toEqual([SITE_1, SITE_2, SITE_3]);
612
+ });
613
+
614
+ test("another project's monitors, severity or labels are refused", async () => {
615
+ await expect(
616
+ preview(draft({ monitorIds: [OTHER_PROJECT_MONITOR_ID] })),
617
+ ).rejects.toBeInstanceOf(BadDataException);
618
+
619
+ await expect(
620
+ preview(draft({ incidentSeverityId: OTHER_PROJECT_SEVERITY_ID })),
621
+ ).rejects.toThrow("incident severity was not found");
622
+
623
+ await expect(
624
+ preview(draft({ labelIds: [OTHER_PROJECT_LABEL_ID] })),
625
+ ).rejects.toThrow("labels were not found");
626
+ });
627
+
628
+ test("a custom field value that does not fit its field is refused, as declaring would be", async () => {
629
+ await expect(
630
+ preview(draft({ customFields: { "Customer count": "many" } })),
631
+ ).rejects.toBeInstanceOf(BadDataException);
632
+ });
633
+
634
+ test("only the pages the caller can read are previewed; the rest are counted", async () => {
635
+ readablePageIds = [SITE_1];
636
+
637
+ // {{affectedStatusPages}} names only the pages the caller can read.
638
+ templates[SITE_1] = Object.assign(
639
+ new StatusPageSubscriberNotificationTemplate(),
640
+ {
641
+ templateName: "Branded",
642
+ templateBody: "<p>{{affectedStatusPages}}</p>",
643
+ },
644
+ );
645
+
646
+ const build: SubscriberNotificationPreviewBuild = await preview(draft());
647
+
648
+ expect(pageIds(build)).toEqual([SITE_1]);
649
+ expect(build.audience.hiddenStatusPageCount).toBe(2);
650
+ expect(build.nothingSentReason).toBeNull();
651
+ expect(body(build.statusPages[0]!)["body"]).toBe("<p>Site 01 Status</p>");
652
+ });
653
+ });
654
+
655
+ describe("nothing will be sent", () => {
656
+ test("a draft with no monitors previews as nothing will be sent, and builds no email", async () => {
657
+ const build: SubscriberNotificationPreviewBuild = await preview(
658
+ draft({ monitorIds: [] }),
659
+ );
660
+
661
+ expect(build.nothingSentReason).toBe(
662
+ SubscriberNotificationPreviewNothingSentReason.NoMonitors,
663
+ );
664
+ expect(build.statusPages).toEqual([]);
665
+ expect(
666
+ StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage,
667
+ ).not.toHaveBeenCalled();
668
+ });
669
+
670
+ test("a private draft, or one that does not notify, sends nothing", async () => {
671
+ const privateBuild: SubscriberNotificationPreviewBuild = await preview(
672
+ draft({ isPrivate: true }),
673
+ );
674
+
675
+ expect(privateBuild.nothingSentReason).toBe(
676
+ SubscriberNotificationPreviewNothingSentReason.PrivateIncident,
677
+ );
678
+ expect(privateBuild.statusPages).toEqual([]);
679
+
680
+ const quietBuild: SubscriberNotificationPreviewBuild = await preview(
681
+ draft({
682
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: false,
683
+ }),
684
+ );
685
+
686
+ expect(quietBuild.nothingSentReason).toBe(
687
+ SubscriberNotificationPreviewNothingSentReason.NotifyOff,
688
+ );
689
+ expect(quietBuild.statusPages).toEqual([]);
690
+ });
691
+
692
+ test("an incident hidden from status pages sends nothing", async () => {
693
+ (IncidentService.findOneBy as unknown as jest.Mock).mockImplementationOnce(
694
+ (async (): Promise<Incident> => {
695
+ const incident: Incident = new Incident();
696
+ incident._id = INCIDENT_ID;
697
+ incident.isVisibleOnStatusPage = false;
698
+ const monitor: Monitor = new Monitor();
699
+ monitor._id = MONITOR_ID;
700
+ incident.monitors = [monitor];
701
+ return incident;
702
+ }) as never,
703
+ );
704
+
705
+ const build: SubscriberNotificationPreviewBuild =
706
+ await preview(publicNote());
707
+
708
+ expect(build.nothingSentReason).toBe(
709
+ SubscriberNotificationPreviewNothingSentReason.HiddenFromStatusPages,
710
+ );
711
+ expect(build.statusPages).toEqual([]);
712
+ });
713
+
714
+ test("a status page that is not the project's is refused", async () => {
715
+ await expect(
716
+ preview(
717
+ draft({ statusPageIds: ["b0000000-0000-4000-8000-000000000077"] }),
718
+ ),
719
+ ).rejects.toThrow("status pages were not found");
720
+ });
721
+
722
+ test("no page that lists the monitors will show it", async () => {
723
+ (
724
+ StatusPageResourceService.findByMonitors as unknown as jest.Mock
725
+ ).mockImplementation((async (): Promise<Array<StatusPageResource>> => {
726
+ return [];
727
+ }) as never);
728
+
729
+ const build: SubscriberNotificationPreviewBuild = await preview(draft());
730
+
731
+ expect(build.nothingSentReason).toBe(
732
+ SubscriberNotificationPreviewNothingSentReason.NoStatusPages,
733
+ );
734
+ expect(build.statusPages).toEqual([]);
735
+ });
736
+ });
737
+
738
+ describe("the emails", () => {
739
+ test("a draft: each readable page's 'incident created' email, its counts and why its template was chosen", async () => {
740
+ templates[SITE_1] = Object.assign(
741
+ new StatusPageSubscriberNotificationTemplate(),
742
+ {
743
+ templateName: "Site 01 branded",
744
+ templateBody:
745
+ "<h1>{{incidentTitle}}</h1>{{incidentDescription}}<p>{{incidentLabels}} / {{incidentSeverity}}</p><a href='{{unsubscribeUrl}}'>x</a>",
746
+ emailSubject: "{{statusPageName}}: {{incidentTitle}}",
747
+ },
748
+ );
749
+ templates[SITE_2] = Object.assign(
750
+ new StatusPageSubscriberNotificationTemplate(),
751
+ {
752
+ templateName: "Site 02 branded",
753
+ templateBody: "<p>{{incidentTitle}}</p>",
754
+ },
755
+ );
756
+
757
+ const build: SubscriberNotificationPreviewBuild = await preview(
758
+ draft({ statusPageIds: [SITE_1, SITE_2] }),
759
+ );
760
+
761
+ expect(build.event).toBe(
762
+ SubscriberNotificationPreviewEvent.IncidentCreated,
763
+ );
764
+ expect(build.nothingSentReason).toBeNull();
765
+ expect(build.audience.isScoped).toBe(true);
766
+ expect(pageIds(build)).toEqual([SITE_1, SITE_2]);
767
+
768
+ const [site1, site2] = build.statusPages as [
769
+ SubscriberNotificationPreviewPage,
770
+ SubscriberNotificationPreviewPage,
771
+ ];
772
+
773
+ // Site 01 has its own SMTP server: its custom template is used.
774
+ expect(site1.name).toBe("Site 01");
775
+ expect(site1.subscriberCounts).toEqual(COUNTS[SITE_1]);
776
+ expect(site1.templateChoice).toEqual({
777
+ usesCustomTemplate: true,
778
+ reason: SubscriberEmailTemplateChoiceReason.CustomTemplate,
779
+ customTemplateName: "Site 01 branded",
780
+ });
781
+ expect(site1.email.subject).toBe(
782
+ "Site 01 Status: Checkout requests failing",
783
+ );
784
+ expect(site1.email.envelope.templateType).toBe(
785
+ EmailTemplateType.BlankTemplate,
786
+ );
787
+ const site1Body: string = String(body(site1)["body"]);
788
+ expect(site1Body).toContain("<h1>Checkout requests failing</h1>");
789
+ expect(site1Body).toContain(
790
+ "<p>Payments fail in <strong>Europe</strong>.</p>",
791
+ );
792
+ expect(site1Body).toContain("<p>Europe / Major</p>");
793
+ expect(site1Body).toContain(
794
+ `<a href='https://status.acme.com/${SITE_1}/unsubscribe/preview'>x</a>`,
795
+ );
796
+
797
+ // Site 02 does not: the default email, and why.
798
+ expect(site2.templateChoice).toEqual({
799
+ usesCustomTemplate: false,
800
+ reason: SubscriberEmailTemplateChoiceReason.CustomTemplateNeedsCustomSmtp,
801
+ customTemplateName: "Site 02 branded",
802
+ });
803
+ expect(site2.email.envelope.templateType).toBe(
804
+ EmailTemplateType.SubscriberIncidentCreated,
805
+ );
806
+ expect(site2.email.subject).toBe("[Incident] Checkout requests failing");
807
+ expect(body(site2)["statusPageName"]).toBe("Site 02 Status");
808
+ expect(body(site2)["incidentSeverity"]).toBe("Major");
809
+ expect(body(site2)["detailsUrl"]).toBe(`https://status.acme.com/${SITE_2}`);
810
+ expect(body(site2)["unsubscribeUrl"]).toBe(
811
+ SubscriberNotificationPreview.getPreviewUnsubscribeUrl(
812
+ `https://status.acme.com/${SITE_2}`,
813
+ ),
814
+ );
815
+ expect(body(site2)["resourcesAffected"]).toBe("Checkout on Site 02");
816
+ });
817
+
818
+ test("a draft's custom field values reach its email, after the mappings a declare would add", async () => {
819
+ const build: SubscriberNotificationPreviewBuild = await preview(
820
+ draft({
821
+ customFields: {
822
+ "Impact details": "Only **card** payments.",
823
+ "Customer count": 1200,
824
+ },
825
+ }),
826
+ );
827
+
828
+ const rows: Array<JSONObject> = body(build.statusPages[0]!)[
829
+ "customFieldRows"
830
+ ] as unknown as Array<JSONObject>;
831
+
832
+ expect(
833
+ rows.map((row: JSONObject): unknown => {
834
+ return row["title"];
835
+ }),
836
+ ).toEqual(["Impact details", "Customer count"]);
837
+ expect(String(rows[0]!["renderedHtml"])).toContain("<strong>card</strong>");
838
+ expect(rows[1]!["plainText"]).toBe("1200");
839
+
840
+ expect(applyMappingsToCreate).toHaveBeenCalledTimes(1);
841
+ const createBy: { data: Incident } = (
842
+ applyMappingsToCreate.mock.calls[0]![0] as {
843
+ createBy: { data: Incident };
844
+ }
845
+ ).createBy;
846
+ expect(
847
+ (createBy.data.monitors || []).map((monitor: Monitor): string => {
848
+ return monitor._id!.toString();
849
+ }),
850
+ ).toEqual([MONITOR_ID]);
851
+ });
852
+
853
+ test("reach is the scope helper's: an incident limited to one page previews only that page", async () => {
854
+ storedScope = [SITE_2];
855
+
856
+ const build: SubscriberNotificationPreviewBuild =
857
+ await preview(publicNote());
858
+
859
+ expect(build.audience.isScoped).toBe(true);
860
+ expect(pageIds(build)).toEqual([SITE_2]);
861
+ expect(
862
+ build.audience.excludedStatusPages.map(
863
+ (page: { statusPageId: string }): string => {
864
+ return page.statusPageId;
865
+ },
866
+ ),
867
+ ).toEqual([SITE_1, SITE_3]);
868
+ });
869
+
870
+ test("a public note: each page's 'incident update' email, with the note being written", async () => {
871
+ const build: SubscriberNotificationPreviewBuild =
872
+ await preview(publicNote());
873
+
874
+ expect(pageIds(build)).toEqual([SITE_1, SITE_2, SITE_3]);
875
+
876
+ for (const page of build.statusPages) {
877
+ expect(page.email.envelope.templateType).toBe(
878
+ EmailTemplateType.SubscriberIncidentNoteCreated,
879
+ );
880
+ expect(page.email.subject).toBe(
881
+ "[Update Incident] Checkout requests failing",
882
+ );
883
+ expect(String(body(page)["note"])).toContain(
884
+ "<strong>rolling back</strong>",
885
+ );
886
+ expect(body(page)["detailsUrl"]).toBe(
887
+ `https://status.acme.com/${page.statusPageId}/incidents/${INCIDENT_ID}`,
888
+ );
889
+ }
890
+ });
891
+
892
+ test("onlyStatusPageId builds that page's email alone", async () => {
893
+ const build: SubscriberNotificationPreviewBuild = await preview(draft(), {
894
+ onlyStatusPageId: SITE_3.toUpperCase(),
895
+ });
896
+
897
+ expect(pageIds(build)).toEqual([SITE_3]);
898
+ // The audience still covers every page.
899
+ expect(build.audience.statusPages).toHaveLength(3);
900
+ });
901
+
902
+ test("a page the caller cannot read cannot be picked", async () => {
903
+ readablePageIds = [SITE_1];
904
+
905
+ const build: SubscriberNotificationPreviewBuild = await preview(draft(), {
906
+ onlyStatusPageId: SITE_2,
907
+ });
908
+
909
+ expect(build.statusPages).toEqual([]);
910
+ });
911
+ });
912
+
913
+ describe("no side effects, and no addresses", () => {
914
+ test("no subscriber is read and no custom field image is made public", async () => {
915
+ await preview(publicNote());
916
+ await preview(
917
+ draft({ customFields: { "Impact details": `![x](${IMAGE_URL})` } }),
918
+ );
919
+
920
+ expect(getSubscribersByStatusPage).not.toHaveBeenCalled();
921
+ expect(subscriberFindBy).not.toHaveBeenCalled();
922
+ expect(syncIsPublicForMarkdownImages).not.toHaveBeenCalled();
923
+ });
924
+
925
+ test("nothing address-shaped is in the preview", async () => {
926
+ const build: SubscriberNotificationPreviewBuild = await preview(draft());
927
+
928
+ const serialized: string = JSON.stringify(
929
+ build.statusPages.map((page: SubscriberNotificationPreviewPage) => {
930
+ return {
931
+ name: page.name,
932
+ counts: page.subscriberCounts,
933
+ email: page.email,
934
+ templateChoice: page.templateChoice,
935
+ };
936
+ }),
937
+ );
938
+
939
+ expect(serialized).not.toMatch(/[\w.+-]+@[\w-]+\.[\w.]+/);
940
+ });
941
+ });
942
+
943
+ describe("parsing requests", () => {
944
+ test("a draft: ids lower-cased, notify on unless switched off", () => {
945
+ const request: SubscriberNotificationPreviewRequest =
946
+ SubscriberNotificationPreviewBuilder.parseRequest({
947
+ event: "IncidentCreated",
948
+ incident: {
949
+ title: "Down",
950
+ monitorIds: [MONITOR_ID.toUpperCase()],
951
+ statusPageIds: [SITE_1],
952
+ labelIds: [],
953
+ incidentSeverityId: "",
954
+ },
955
+ });
956
+
957
+ expect(request).toEqual({
958
+ event: SubscriberNotificationPreviewEvent.IncidentCreated,
959
+ incident: {
960
+ title: "Down",
961
+ description: "",
962
+ incidentSeverityId: null,
963
+ monitorIds: [MONITOR_ID],
964
+ statusPageIds: [SITE_1],
965
+ labelIds: [],
966
+ customFields: {},
967
+ isPrivate: false,
968
+ shouldStatusPageSubscribersBeNotifiedOnIncidentCreated: true,
969
+ },
970
+ });
971
+ });
972
+
973
+ test("a public note, with when it says it was posted", () => {
974
+ const request: SubscriberNotificationPreviewRequest =
975
+ SubscriberNotificationPreviewBuilder.parseRequest({
976
+ event: "IncidentPublicNoteCreated",
977
+ incidentId: INCIDENT_ID,
978
+ note: "Update",
979
+ postedAt: "2026-09-27T10:30:00.000Z",
980
+ });
981
+
982
+ expect(request.event).toBe(
983
+ SubscriberNotificationPreviewEvent.IncidentPublicNoteCreated,
984
+ );
985
+ expect((request as { postedAt: Date | null }).postedAt?.toISOString()).toBe(
986
+ "2026-09-27T10:30:00.000Z",
987
+ );
988
+ });
989
+
990
+ test.each([
991
+ [{}, "event must be one of"],
992
+ [{ event: "Nope" }, "event must be one of"],
993
+ [{ event: "IncidentCreated" }, "incident must be an object"],
994
+ [
995
+ { event: "IncidentCreated", incident: { monitorIds: "x" } },
996
+ "must be a list of IDs",
997
+ ],
998
+ [
999
+ { event: "IncidentCreated", incident: { monitorIds: ["not-an-id"] } },
1000
+ "must be a valid ID",
1001
+ ],
1002
+ [
1003
+ { event: "IncidentCreated", incident: { title: 42 } },
1004
+ "incident.title must be text",
1005
+ ],
1006
+ [
1007
+ { event: "IncidentCreated", incident: { customFields: [] } },
1008
+ "incident.customFields must be an object",
1009
+ ],
1010
+ [
1011
+ { event: "IncidentPublicNoteCreated", incidentId: "x" },
1012
+ "incidentId must be a valid ID",
1013
+ ],
1014
+ [
1015
+ {
1016
+ event: "IncidentPublicNoteCreated",
1017
+ incidentId: INCIDENT_ID,
1018
+ postedAt: "not a date",
1019
+ },
1020
+ "postedAt must be a date",
1021
+ ],
1022
+ ])("refuses %j", (body: JSONObject, message: string) => {
1023
+ expect(() => {
1024
+ return SubscriberNotificationPreviewBuilder.parseRequest(body);
1025
+ }).toThrow(message);
1026
+ });
1027
+
1028
+ test("too long a note is refused", () => {
1029
+ expect(() => {
1030
+ return SubscriberNotificationPreviewBuilder.parseRequest({
1031
+ event: "IncidentPublicNoteCreated",
1032
+ incidentId: INCIDENT_ID,
1033
+ note: "x".repeat(SubscriberNotificationPreview.maxTextLength + 1),
1034
+ });
1035
+ }).toThrow("at most");
1036
+ });
1037
+
1038
+ /*
1039
+ * Every custom field value is rendered, Markdown included, and goes into
1040
+ * every page's email. The title and description were capped; the values
1041
+ * were not, so one request could make the server render as much as it
1042
+ * could be sent.
1043
+ */
1044
+ describe("an incident's custom field values are bounded", () => {
1045
+ function draftWith(customFields: JSONObject): JSONObject {
1046
+ return {
1047
+ event: "IncidentCreated",
1048
+ incident: { title: "Down", customFields: customFields },
1049
+ };
1050
+ }
1051
+
1052
+ test("values a project really has are taken as they are", () => {
1053
+ const customFields: JSONObject = {
1054
+ Impact: "High",
1055
+ "Expected Resolution": "2026-09-27T14:00:00.000Z",
1056
+ "Estimated Duration": 90,
1057
+ Acknowledgement: true,
1058
+ "Affected Locations": ["Site 03", "Site 07"],
1059
+ "Additional Information": "x".repeat(20000),
1060
+ };
1061
+
1062
+ const request: SubscriberNotificationPreviewRequest =
1063
+ SubscriberNotificationPreviewBuilder.parseRequest(
1064
+ draftWith(customFields),
1065
+ );
1066
+
1067
+ expect(
1068
+ (request as { incident: { customFields: JSONObject } }).incident
1069
+ .customFields,
1070
+ ).toEqual(customFields);
1071
+ });
1072
+
1073
+ test("too many values are refused", () => {
1074
+ const customFields: JSONObject = {};
1075
+ for (
1076
+ let index: number = 0;
1077
+ index <= SubscriberNotificationPreview.maxCustomFieldCount;
1078
+ index++
1079
+ ) {
1080
+ customFields[`Field ${index}`] = "x";
1081
+ }
1082
+
1083
+ expect(() => {
1084
+ return SubscriberNotificationPreviewBuilder.parseRequest(
1085
+ draftWith(customFields),
1086
+ );
1087
+ }).toThrow(
1088
+ `incident.customFields can have at most ${SubscriberNotificationPreview.maxCustomFieldCount} values.`,
1089
+ );
1090
+ });
1091
+
1092
+ test("one value too long is refused", () => {
1093
+ expect(() => {
1094
+ return SubscriberNotificationPreviewBuilder.parseRequest(
1095
+ draftWith({
1096
+ "Additional Information": "x".repeat(
1097
+ SubscriberNotificationPreview.maxCustomFieldsLength,
1098
+ ),
1099
+ }),
1100
+ );
1101
+ }).toThrow(
1102
+ `incident.customFields can be at most ${SubscriberNotificationPreview.maxCustomFieldsLength} characters long in all.`,
1103
+ );
1104
+ });
1105
+
1106
+ test("values too long together are refused, however they are split", () => {
1107
+ const customFields: JSONObject = {};
1108
+ const perField: number = Math.ceil(
1109
+ SubscriberNotificationPreview.maxCustomFieldsLength / 10,
1110
+ );
1111
+ for (let index: number = 0; index < 11; index++) {
1112
+ customFields[`Field ${index}`] = "x".repeat(perField);
1113
+ }
1114
+
1115
+ expect(() => {
1116
+ return SubscriberNotificationPreviewBuilder.parseRequest(
1117
+ draftWith(customFields),
1118
+ );
1119
+ }).toThrow("characters long in all");
1120
+ });
1121
+
1122
+ test("a nested value counts toward the length too", () => {
1123
+ expect(() => {
1124
+ return SubscriberNotificationPreviewBuilder.parseRequest(
1125
+ draftWith({
1126
+ "Affected Locations": new Array(
1127
+ SubscriberNotificationPreview.maxCustomFieldsLength,
1128
+ ).fill("x"),
1129
+ }),
1130
+ );
1131
+ }).toThrow("characters long in all");
1132
+ });
1133
+
1134
+ test("so does a test send's", () => {
1135
+ expect(() => {
1136
+ return SubscriberNotificationPreviewBuilder.parseSendTestRequest({
1137
+ ...draftWith({
1138
+ Notes: "x".repeat(
1139
+ SubscriberNotificationPreview.maxCustomFieldsLength,
1140
+ ),
1141
+ }),
1142
+ statusPageId: SITE_1,
1143
+ });
1144
+ }).toThrow("characters long in all");
1145
+ });
1146
+ });
1147
+
1148
+ test("a test send names a status page, and no address is read from the body", () => {
1149
+ const request: SubscriberNotificationSendTestRequest =
1150
+ SubscriberNotificationPreviewBuilder.parseSendTestRequest({
1151
+ event: "IncidentPublicNoteCreated",
1152
+ incidentId: INCIDENT_ID,
1153
+ note: "Update",
1154
+ statusPageId: SITE_1,
1155
+ toEmail: "someone-else@example.com",
1156
+ });
1157
+
1158
+ expect(request.statusPageId).toBe(SITE_1);
1159
+ expect(JSON.stringify(request)).not.toContain("someone-else");
1160
+
1161
+ expect(() => {
1162
+ return SubscriberNotificationPreviewBuilder.parseSendTestRequest({
1163
+ event: "IncidentPublicNoteCreated",
1164
+ incidentId: INCIDENT_ID,
1165
+ });
1166
+ }).toThrow("statusPageId must be a valid ID");
1167
+ });
1168
+ });