@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,1408 @@
1
+ import { mockRouter } from "./Helpers";
2
+ import CommonAPI from "../../../Server/API/CommonAPI";
3
+ import IncidentAPI from "../../../Server/API/IncidentAPI";
4
+ import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
5
+ import IncidentService from "../../../Server/Services/IncidentService";
6
+ import MonitorService from "../../../Server/Services/MonitorService";
7
+ import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
8
+ import StatusPageService from "../../../Server/Services/StatusPageService";
9
+ import StatusPageSubscriberService from "../../../Server/Services/StatusPageSubscriberService";
10
+ import {
11
+ ExpressRequest,
12
+ ExpressResponse,
13
+ NextFunction,
14
+ } from "../../../Server/Utils/Express";
15
+ import Response from "../../../Server/Utils/Response";
16
+ import Incident from "../../../Models/DatabaseModels/Incident";
17
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
18
+ import StatusPage from "../../../Models/DatabaseModels/StatusPage";
19
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
20
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
21
+ import Dictionary from "../../../Types/Dictionary";
22
+ import BadDataException from "../../../Types/Exception/BadDataException";
23
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
24
+ import NotFoundException from "../../../Types/Exception/NotFoundException";
25
+ import { JSONObject } from "../../../Types/JSON";
26
+ import ObjectID from "../../../Types/ObjectID";
27
+ import Permission, {
28
+ UserPermission,
29
+ UserTenantAccessPermission,
30
+ } from "../../../Types/Permission";
31
+ import IncidentSubscriberAudience, {
32
+ IncidentSubscriberAudienceCounts,
33
+ IncidentSubscriberAudienceExclusionReason,
34
+ IncidentSubscriberAudienceResult,
35
+ } from "../../../Types/StatusPage/IncidentSubscriberAudience";
36
+ import getJestMockFunction, { MockFunction } from "../../MockType";
37
+ import {
38
+ afterEach,
39
+ beforeAll,
40
+ beforeEach,
41
+ describe,
42
+ expect,
43
+ jest,
44
+ test,
45
+ } from "@jest/globals";
46
+
47
+ /*
48
+ * POST /incident/subscriber-audience: who an incident's status page
49
+ * notifications would reach, shown before an incident is declared or a public
50
+ * note is posted.
51
+ *
52
+ * The route runs for real down to the services: the scope helper
53
+ * (IncidentStatusPageScope) and the audience builder are not stubbed, only
54
+ * the rows they read - status page resources by monitor, the status pages,
55
+ * the incident, and the subscriber counts. What is pinned down:
56
+ *
57
+ * - the counts: one entry per notified page, per channel, in name order;
58
+ * scope, scoped-only pages and pages that hide incidents are applied as
59
+ * the jobs apply them;
60
+ * - no address ever leaves: subscribers are counted with one aggregate and
61
+ * never read, and nothing address-shaped is in the answer;
62
+ * - a status page the caller cannot read is never named nor counted - the
63
+ * notified ones collapse into hiddenStatusPageCount;
64
+ * - the project is enforced: another project's incident is not found, and
65
+ * another project's monitors or status pages are refused before anything
66
+ * is resolved;
67
+ * - the permission check, and the request's shape.
68
+ */
69
+
70
+ jest.mock("../../../Server/Utils/Express", () => {
71
+ return {
72
+ getRouter: () => {
73
+ return mockRouter;
74
+ },
75
+ };
76
+ });
77
+
78
+ jest.mock("../../../Server/Utils/Response", () => {
79
+ return {
80
+ sendJsonObjectResponse: jest.fn(),
81
+ sendEntityArrayResponse: jest.fn(),
82
+ sendEntityResponse: jest.fn(),
83
+ sendEmptySuccessResponse: jest.fn(),
84
+ sendErrorResponse: jest.fn(),
85
+ sendFileResponse: jest.fn(),
86
+ setNoCacheHeaders: jest.fn(),
87
+ };
88
+ });
89
+
90
+ const ROUTE: string = "/incident/subscriber-audience";
91
+
92
+ const PROJECT_ID: ObjectID = new ObjectID(
93
+ "10000000-0000-4000-8000-000000000001",
94
+ );
95
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
96
+ "10000000-0000-4000-8000-000000000002",
97
+ );
98
+ const USER_ID: ObjectID = new ObjectID("20000000-0000-4000-8000-000000000001");
99
+
100
+ const INCIDENT_ID: string = "a0000000-0000-4000-8000-00000000000a";
101
+
102
+ const SHARED_MONITOR: string = "c0000000-0000-4000-8000-000000000001";
103
+ const LONELY_MONITOR: string = "c0000000-0000-4000-8000-000000000002";
104
+
105
+ // Five site pages; each lists the shared monitor.
106
+ const SITE_PAGE_IDS: Array<string> = [1, 2, 3, 4, 5].map(
107
+ (site: number): string => {
108
+ return `b0000000-0000-4000-8000-0000000000${site.toString().padStart(2, "0")}`;
109
+ },
110
+ );
111
+
112
+ // A page that lists only the lonely monitor.
113
+ const LONELY_PAGE_ID: string = "b0000000-0000-4000-8000-000000000099";
114
+
115
+ function sitePageId(site: number): string {
116
+ return SITE_PAGE_IDS[site - 1]!;
117
+ }
118
+
119
+ function siteName(site: number): string {
120
+ return `Site ${site.toString().padStart(2, "0")}`;
121
+ }
122
+
123
+ interface PageFixture {
124
+ id: string;
125
+ name: string;
126
+ projectId?: ObjectID | undefined;
127
+ onlyShowScopedIncidents?: boolean | undefined;
128
+ showIncidentsOnStatusPage?: boolean | undefined;
129
+ }
130
+
131
+ interface ResourceFixture {
132
+ statusPageId: string;
133
+ monitorId: string;
134
+ }
135
+
136
+ interface IncidentFixture {
137
+ id: string;
138
+ projectId: ObjectID;
139
+ monitorIds: Array<string>;
140
+ isVisibleOnStatusPage: boolean;
141
+ isPrivate: boolean;
142
+ scopedStatusPageIds: Array<string> | null;
143
+ // The record of pages already sent its 'created' notification in full.
144
+ statusPagesNotifiedOnCreation?: Array<string> | undefined;
145
+ }
146
+
147
+ let pages: Array<PageFixture> = [];
148
+ let resources: Array<ResourceFixture> = [];
149
+ let incidents: Array<IncidentFixture> = [];
150
+ let monitorProjects: Dictionary<ObjectID> = {};
151
+ // The pages the caller may read; null reads all of them.
152
+ let readablePageIds: Array<string> | null = null;
153
+ // The caller has no status page read access at all.
154
+ let statusPageReadRefused: boolean = false;
155
+ let subscriberCounts: Dictionary<IncidentSubscriberAudienceCounts> = {};
156
+
157
+ let findByMonitors: MockFunction;
158
+ let countActiveSubscribersByChannel: MockFunction;
159
+ let subscriberFindBy: MockFunction;
160
+ let getSubscribersByStatusPage: MockFunction;
161
+ let incidentFindOneBy: MockFunction;
162
+
163
+ let callerProps: DatabaseCommonInteractionProps;
164
+
165
+ // The ids QueryHelper.any was given (it builds a Raw IN operator).
166
+ function idsIn(operator: unknown): Array<string> {
167
+ const raw: { objectLiteralParameters?: Dictionary<unknown> } = operator as {
168
+ objectLiteralParameters?: Dictionary<unknown>;
169
+ };
170
+
171
+ return (
172
+ Object.values(raw.objectLiteralParameters || {}) as Array<
173
+ Array<string | ObjectID>
174
+ >
175
+ )
176
+ .flat()
177
+ .map((id: string | ObjectID): string => {
178
+ return id.toString().toLowerCase();
179
+ });
180
+ }
181
+
182
+ function sameId(a: unknown, b: unknown): boolean {
183
+ return (
184
+ String(a?.toString() || "").toLowerCase() ===
185
+ String(b?.toString() || "").toLowerCase()
186
+ );
187
+ }
188
+
189
+ function permissionProps(
190
+ grants: Array<Permission>,
191
+ options: { blocked?: Array<Permission> } = {},
192
+ ): DatabaseCommonInteractionProps {
193
+ const permissions: Array<UserPermission> = [
194
+ ...grants.map((permission: Permission): UserPermission => {
195
+ return {
196
+ _type: "UserPermission",
197
+ permission: permission,
198
+ labelIds: [],
199
+ isBlockPermission: false,
200
+ } as UserPermission;
201
+ }),
202
+ ...(options.blocked || []).map((permission: Permission): UserPermission => {
203
+ return {
204
+ _type: "UserPermission",
205
+ permission: permission,
206
+ labelIds: [],
207
+ isBlockPermission: true,
208
+ } as UserPermission;
209
+ }),
210
+ ];
211
+
212
+ const tenantPermission: UserTenantAccessPermission = {
213
+ _type: "UserTenantAccessPermission",
214
+ projectId: PROJECT_ID,
215
+ permissions: permissions,
216
+ } as UserTenantAccessPermission;
217
+
218
+ const permissionMap: Dictionary<UserTenantAccessPermission> = {};
219
+ permissionMap[PROJECT_ID.toString()] = tenantPermission;
220
+
221
+ return {
222
+ userId: USER_ID,
223
+ tenantId: PROJECT_ID,
224
+ userTenantAccessPermission: permissionMap,
225
+ };
226
+ }
227
+
228
+ function makePage(fixture: PageFixture): StatusPage {
229
+ const page: StatusPage = new StatusPage();
230
+ page._id = fixture.id;
231
+ page.name = fixture.name;
232
+ page.projectId = fixture.projectId || PROJECT_ID;
233
+ page.onlyShowScopedIncidents = fixture.onlyShowScopedIncidents ?? false;
234
+ page.showIncidentsOnStatusPage = fixture.showIncidentsOnStatusPage ?? true;
235
+ return page;
236
+ }
237
+
238
+ function counts(
239
+ partial: Partial<IncidentSubscriberAudienceCounts>,
240
+ ): IncidentSubscriberAudienceCounts {
241
+ return {
242
+ ...IncidentSubscriberAudience.getEmptyCounts(),
243
+ ...partial,
244
+ };
245
+ }
246
+
247
+ // Five site pages on the shared monitor, and one on the lonely monitor.
248
+ function useSitePages(
249
+ options: { onlyShowScopedIncidents?: boolean } = {},
250
+ ): void {
251
+ pages = [
252
+ ...SITE_PAGE_IDS.map((id: string, index: number): PageFixture => {
253
+ return {
254
+ id: id,
255
+ name: siteName(index + 1),
256
+ onlyShowScopedIncidents: options.onlyShowScopedIncidents ?? false,
257
+ };
258
+ }),
259
+ {
260
+ id: LONELY_PAGE_ID,
261
+ name: "Lonely",
262
+ },
263
+ ];
264
+
265
+ resources = [
266
+ ...SITE_PAGE_IDS.map((id: string): ResourceFixture => {
267
+ return { statusPageId: id, monitorId: SHARED_MONITOR };
268
+ }),
269
+ { statusPageId: LONELY_PAGE_ID, monitorId: LONELY_MONITOR },
270
+ ];
271
+
272
+ monitorProjects = {
273
+ [SHARED_MONITOR]: PROJECT_ID,
274
+ [LONELY_MONITOR]: PROJECT_ID,
275
+ };
276
+
277
+ subscriberCounts = {
278
+ [sitePageId(1)]: counts({ email: 41, sms: 3 }),
279
+ [sitePageId(2)]: counts({ email: 18 }),
280
+ [sitePageId(3)]: counts({ email: 7, slack: 1, webhook: 2 }),
281
+ [sitePageId(4)]: counts({ microsoftTeams: 4 }),
282
+ [LONELY_PAGE_ID]: counts({ email: 99 }),
283
+ // Site 05 has no active subscribers: no row.
284
+ };
285
+ }
286
+
287
+ interface RouteCall {
288
+ thrown: unknown;
289
+ sent: JSONObject | undefined;
290
+ }
291
+
292
+ async function post(body: unknown): Promise<RouteCall> {
293
+ const req: ExpressRequest = {
294
+ params: {},
295
+ query: {},
296
+ body: body,
297
+ headers: {},
298
+ } as unknown as ExpressRequest;
299
+
300
+ const res: ExpressResponse = {} as ExpressResponse;
301
+ const next: MockFunction = getJestMockFunction();
302
+
303
+ await mockRouter
304
+ .match("post", ROUTE)
305
+ .handlerFunction(req, res, next as unknown as NextFunction);
306
+
307
+ const send: jest.Mock =
308
+ Response.sendJsonObjectResponse as unknown as jest.Mock;
309
+
310
+ return {
311
+ thrown: next.mock.calls[0] ? next.mock.calls[0][0] : undefined,
312
+ sent: send.mock.calls[0]
313
+ ? (send.mock.calls[0][2] as JSONObject)
314
+ : undefined,
315
+ };
316
+ }
317
+
318
+ async function audienceFor(
319
+ body: unknown,
320
+ ): Promise<IncidentSubscriberAudienceResult> {
321
+ const call: RouteCall = await post(body);
322
+
323
+ if (call.thrown) {
324
+ throw call.thrown;
325
+ }
326
+
327
+ expect(call.sent).toBeDefined();
328
+
329
+ return IncidentSubscriberAudience.fromJSON(call.sent!);
330
+ }
331
+
332
+ function draft(
333
+ monitorIds: Array<string>,
334
+ statusPageIds: Array<string> = [],
335
+ ): JSONObject {
336
+ return {
337
+ monitorIds: monitorIds,
338
+ statusPageIds: statusPageIds,
339
+ };
340
+ }
341
+
342
+ function names(list: Array<{ name: string }>): Array<string> {
343
+ return list.map((item: { name: string }): string => {
344
+ return item.name;
345
+ });
346
+ }
347
+
348
+ beforeAll(() => {
349
+ mockRouter.routes.length = 0;
350
+ new IncidentAPI();
351
+ });
352
+
353
+ beforeEach(() => {
354
+ jest.clearAllMocks();
355
+
356
+ pages = [];
357
+ resources = [];
358
+ incidents = [];
359
+ monitorProjects = {};
360
+ readablePageIds = null;
361
+ statusPageReadRefused = false;
362
+ subscriberCounts = {};
363
+
364
+ callerProps = permissionProps([Permission.ProjectMember]);
365
+
366
+ jest
367
+ .spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
368
+ .mockImplementation((() => {
369
+ return Promise.resolve(callerProps);
370
+ }) as never);
371
+
372
+ findByMonitors = getJestMockFunction();
373
+ findByMonitors.mockImplementation(
374
+ (data: {
375
+ monitorIds: Array<ObjectID>;
376
+ }): Promise<Array<StatusPageResource>> => {
377
+ const monitorIds: Array<string> = data.monitorIds.map(
378
+ (id: ObjectID): string => {
379
+ return id.toString().toLowerCase();
380
+ },
381
+ );
382
+
383
+ return Promise.resolve(
384
+ resources
385
+ .filter((resource: ResourceFixture): boolean => {
386
+ return monitorIds.includes(resource.monitorId);
387
+ })
388
+ .map((resource: ResourceFixture, index: number) => {
389
+ const model: StatusPageResource = new StatusPageResource();
390
+ model._id = `e0000000-0000-4000-8000-${index.toString().padStart(12, "0")}`;
391
+ model.statusPageId = new ObjectID(resource.statusPageId);
392
+ return model;
393
+ }),
394
+ );
395
+ },
396
+ );
397
+ jest
398
+ .spyOn(StatusPageResourceService, "findByMonitors")
399
+ .mockImplementation(findByMonitors as never);
400
+
401
+ jest
402
+ .spyOn(StatusPageSubscriberService, "getStatusPagesToSendNotification")
403
+ .mockImplementation(((ids: Array<ObjectID>): Promise<Array<StatusPage>> => {
404
+ const wanted: Array<string> = ids.map((id: ObjectID): string => {
405
+ return id.toString().toLowerCase();
406
+ });
407
+
408
+ return Promise.resolve(
409
+ pages
410
+ .filter((page: PageFixture): boolean => {
411
+ return wanted.includes(page.id);
412
+ })
413
+ .map(makePage),
414
+ );
415
+ }) as never);
416
+
417
+ // Root reads check the project; the caller's reads apply what they may read.
418
+ jest.spyOn(StatusPageService, "findBy").mockImplementation(((findBy: {
419
+ query: JSONObject;
420
+ props: DatabaseCommonInteractionProps;
421
+ }): Promise<Array<StatusPage>> => {
422
+ const wanted: Array<string> = idsIn(findBy.query["_id"]);
423
+
424
+ if (!findBy.props.isRoot && statusPageReadRefused) {
425
+ return Promise.reject(
426
+ new NotAuthorizedException(
427
+ "You do not have permissions to read Status Page.",
428
+ ),
429
+ );
430
+ }
431
+
432
+ return Promise.resolve(
433
+ pages
434
+ .filter((page: PageFixture): boolean => {
435
+ return (
436
+ wanted.includes(page.id) &&
437
+ sameId(page.projectId || PROJECT_ID, findBy.query["projectId"]) &&
438
+ (findBy.props.isRoot ||
439
+ readablePageIds === null ||
440
+ readablePageIds.includes(page.id))
441
+ );
442
+ })
443
+ .map(makePage),
444
+ );
445
+ }) as never);
446
+
447
+ jest.spyOn(MonitorService, "findBy").mockImplementation(((findBy: {
448
+ query: JSONObject;
449
+ }): Promise<Array<Monitor>> => {
450
+ return Promise.resolve(
451
+ idsIn(findBy.query["_id"])
452
+ .filter((id: string): boolean => {
453
+ return (
454
+ Boolean(monitorProjects[id]) &&
455
+ sameId(monitorProjects[id], findBy.query["projectId"])
456
+ );
457
+ })
458
+ .map((id: string): Monitor => {
459
+ const monitor: Monitor = new Monitor();
460
+ monitor._id = id;
461
+ return monitor;
462
+ }),
463
+ );
464
+ }) as never);
465
+
466
+ // The stored scope, as IncidentStatusPageScope reads it.
467
+ jest.spyOn(IncidentService, "findBy").mockImplementation(((findBy: {
468
+ query: JSONObject;
469
+ }): Promise<Array<Incident>> => {
470
+ const wanted: Array<string> = idsIn(findBy.query["_id"]);
471
+
472
+ return Promise.resolve(
473
+ incidents
474
+ .filter((fixture: IncidentFixture): boolean => {
475
+ return wanted.includes(fixture.id);
476
+ })
477
+ .map((fixture: IncidentFixture): Incident => {
478
+ const incident: Incident = new Incident();
479
+ incident._id = fixture.id;
480
+ incident.isScopedToStatusPages = fixture.scopedStatusPageIds !== null;
481
+ incident.statusPages = (fixture.scopedStatusPageIds || []).map(
482
+ (id: string): StatusPage => {
483
+ const page: StatusPage = new StatusPage();
484
+ page._id = id;
485
+ return page;
486
+ },
487
+ );
488
+ return incident;
489
+ }),
490
+ );
491
+ }) as never);
492
+
493
+ incidentFindOneBy = getJestMockFunction();
494
+ incidentFindOneBy.mockImplementation(
495
+ (findOneBy: { query: JSONObject }): Promise<Incident | null> => {
496
+ const fixture: IncidentFixture | undefined = incidents.find(
497
+ (candidate: IncidentFixture): boolean => {
498
+ return (
499
+ sameId(candidate.id, findOneBy.query["_id"]) &&
500
+ sameId(candidate.projectId, findOneBy.query["projectId"])
501
+ );
502
+ },
503
+ );
504
+
505
+ if (!fixture) {
506
+ return Promise.resolve(null);
507
+ }
508
+
509
+ const incident: Incident = new Incident();
510
+ incident._id = fixture.id;
511
+ incident.isVisibleOnStatusPage = fixture.isVisibleOnStatusPage;
512
+ incident.isPrivate = fixture.isPrivate;
513
+ incident.isScopedToStatusPages = fixture.scopedStatusPageIds !== null;
514
+ incident.statusPages = (fixture.scopedStatusPageIds || []).map(
515
+ (id: string): StatusPage => {
516
+ const page: StatusPage = new StatusPage();
517
+ page._id = id;
518
+ return page;
519
+ },
520
+ );
521
+ incident.monitors = fixture.monitorIds.map((id: string): Monitor => {
522
+ const monitor: Monitor = new Monitor();
523
+ monitor._id = id;
524
+ return monitor;
525
+ });
526
+ if (fixture.statusPagesNotifiedOnCreation) {
527
+ incident.statusPagesNotifiedOnCreation = [
528
+ ...fixture.statusPagesNotifiedOnCreation,
529
+ ];
530
+ }
531
+ return Promise.resolve(incident);
532
+ },
533
+ );
534
+ jest
535
+ .spyOn(IncidentService, "findOneBy")
536
+ .mockImplementation(incidentFindOneBy as never);
537
+
538
+ countActiveSubscribersByChannel = getJestMockFunction();
539
+ countActiveSubscribersByChannel.mockImplementation(
540
+ (data: {
541
+ projectId: ObjectID;
542
+ statusPageIds: Array<ObjectID>;
543
+ }): Promise<Dictionary<IncidentSubscriberAudienceCounts>> => {
544
+ const result: Dictionary<IncidentSubscriberAudienceCounts> = {};
545
+
546
+ for (const id of data.statusPageIds) {
547
+ const key: string = id.toString().toLowerCase();
548
+
549
+ if (subscriberCounts[key] && sameId(data.projectId, PROJECT_ID)) {
550
+ result[key] = subscriberCounts[key]!;
551
+ }
552
+ }
553
+
554
+ return Promise.resolve(result);
555
+ },
556
+ );
557
+ jest
558
+ .spyOn(StatusPageSubscriberService, "countActiveSubscribersByChannel")
559
+ .mockImplementation(countActiveSubscribersByChannel as never);
560
+
561
+ // Neither may ever be reached: they read subscribers out, addresses and all.
562
+ subscriberFindBy = getJestMockFunction();
563
+ jest
564
+ .spyOn(StatusPageSubscriberService, "findBy")
565
+ .mockImplementation(subscriberFindBy as never);
566
+ getSubscribersByStatusPage = getJestMockFunction();
567
+ jest
568
+ .spyOn(StatusPageSubscriberService, "getSubscribersByStatusPage")
569
+ .mockImplementation(getSubscribersByStatusPage as never);
570
+ });
571
+
572
+ afterEach(() => {
573
+ jest.restoreAllMocks();
574
+ });
575
+
576
+ describe("the route", () => {
577
+ test("is registered once, behind the session and the expired-session guard", () => {
578
+ const registrations: Array<ReturnType<typeof mockRouter.match>> =
579
+ mockRouter.routes.filter(
580
+ (route: ReturnType<typeof mockRouter.match>): boolean => {
581
+ return route.method === "POST" && route.uri === ROUTE;
582
+ },
583
+ );
584
+
585
+ expect(registrations).toHaveLength(1);
586
+ expect(registrations[0]!.middlewares[0]).toBe(
587
+ UserMiddleware.getUserMiddleware,
588
+ );
589
+ expect(registrations[0]!.middlewares[1]).toBe(
590
+ UserMiddleware.requireUserAuthentication,
591
+ );
592
+ });
593
+
594
+ test("the path is the one the dashboard calls", () => {
595
+ expect(IncidentSubscriberAudience.apiPath).toBe(ROUTE);
596
+ });
597
+
598
+ test("answers with no-cache headers", async () => {
599
+ useSitePages();
600
+
601
+ await audienceFor(draft([SHARED_MONITOR]));
602
+
603
+ expect(Response.setNoCacheHeaders).toHaveBeenCalledTimes(1);
604
+ });
605
+ });
606
+
607
+ describe("counts for an incident being declared", () => {
608
+ test("an unscoped incident on a shared monitor: every site page, per channel, in name order", async () => {
609
+ useSitePages();
610
+
611
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
612
+ draft([SHARED_MONITOR]),
613
+ );
614
+
615
+ expect(audience.hasMonitors).toBe(true);
616
+ expect(audience.isScoped).toBe(false);
617
+ expect(audience.isHiddenFromStatusPages).toBe(false);
618
+ expect(audience.hiddenStatusPageCount).toBe(0);
619
+ expect(audience.excludedStatusPages).toEqual([]);
620
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([]);
621
+ expect(audience.statusPages).toEqual([
622
+ {
623
+ statusPageId: sitePageId(1),
624
+ name: "Site 01",
625
+ subscriberCounts: counts({ email: 41, sms: 3 }),
626
+ },
627
+ {
628
+ statusPageId: sitePageId(2),
629
+ name: "Site 02",
630
+ subscriberCounts: counts({ email: 18 }),
631
+ },
632
+ {
633
+ statusPageId: sitePageId(3),
634
+ name: "Site 03",
635
+ subscriberCounts: counts({ email: 7, slack: 1, webhook: 2 }),
636
+ },
637
+ {
638
+ statusPageId: sitePageId(4),
639
+ name: "Site 04",
640
+ subscriberCounts: counts({ microsoftTeams: 4 }),
641
+ },
642
+ // No active subscribers: listed, with zero on every channel.
643
+ {
644
+ statusPageId: sitePageId(5),
645
+ name: "Site 05",
646
+ subscriberCounts: counts({}),
647
+ },
648
+ ]);
649
+ });
650
+
651
+ test("scoped to two of the site pages: only those two, and the rest listed as outside the scope", async () => {
652
+ useSitePages();
653
+
654
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
655
+ draft([SHARED_MONITOR], [sitePageId(3), sitePageId(1)]),
656
+ );
657
+
658
+ expect(audience.isScoped).toBe(true);
659
+ expect(names(audience.statusPages)).toEqual(["Site 01", "Site 03"]);
660
+ expect(audience.statusPages[0]!.subscriberCounts).toEqual(
661
+ counts({ email: 41, sms: 3 }),
662
+ );
663
+ expect(audience.excludedStatusPages).toEqual([
664
+ {
665
+ statusPageId: sitePageId(2),
666
+ name: "Site 02",
667
+ reason: IncidentSubscriberAudienceExclusionReason.OutsideIncidentScope,
668
+ },
669
+ {
670
+ statusPageId: sitePageId(4),
671
+ name: "Site 04",
672
+ reason: IncidentSubscriberAudienceExclusionReason.OutsideIncidentScope,
673
+ },
674
+ {
675
+ statusPageId: sitePageId(5),
676
+ name: "Site 05",
677
+ reason: IncidentSubscriberAudienceExclusionReason.OutsideIncidentScope,
678
+ },
679
+ ]);
680
+
681
+ // Subscribers are counted for the notified pages only.
682
+ expect(countActiveSubscribersByChannel).toHaveBeenCalledTimes(1);
683
+ expect(
684
+ (
685
+ countActiveSubscribersByChannel.mock.calls[0]![0] as {
686
+ statusPageIds: Array<ObjectID>;
687
+ }
688
+ ).statusPageIds.map((id: ObjectID): string => {
689
+ return id.toString();
690
+ }),
691
+ ).toEqual([sitePageId(1), sitePageId(3)]);
692
+ });
693
+
694
+ test("an unscoped incident reaches none of the pages that only show scoped incidents", async () => {
695
+ useSitePages({ onlyShowScopedIncidents: true });
696
+
697
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
698
+ draft([SHARED_MONITOR]),
699
+ );
700
+
701
+ expect(audience.statusPages).toEqual([]);
702
+ expect(IncidentSubscriberAudience.reachesAnyone(audience)).toBe(false);
703
+ expect(names(audience.excludedStatusPages)).toEqual([
704
+ "Site 01",
705
+ "Site 02",
706
+ "Site 03",
707
+ "Site 04",
708
+ "Site 05",
709
+ ]);
710
+
711
+ for (const excluded of audience.excludedStatusPages) {
712
+ expect(excluded.reason).toBe(
713
+ IncidentSubscriberAudienceExclusionReason.OnlyShowsScopedIncidents,
714
+ );
715
+ }
716
+
717
+ expect(countActiveSubscribersByChannel).not.toHaveBeenCalled();
718
+ });
719
+
720
+ test("scoping to a page that only shows scoped incidents reaches it", async () => {
721
+ useSitePages({ onlyShowScopedIncidents: true });
722
+
723
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
724
+ draft([SHARED_MONITOR], [sitePageId(2)]),
725
+ );
726
+
727
+ expect(names(audience.statusPages)).toEqual(["Site 02"]);
728
+ expect(audience.statusPages[0]!.subscriberCounts).toEqual(
729
+ counts({ email: 18 }),
730
+ );
731
+ });
732
+
733
+ test("a page that does not show incidents is left out, as the jobs skip it", async () => {
734
+ useSitePages();
735
+ pages = pages.map((page: PageFixture): PageFixture => {
736
+ return page.id === sitePageId(2)
737
+ ? { ...page, showIncidentsOnStatusPage: false }
738
+ : page;
739
+ });
740
+
741
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
742
+ draft([SHARED_MONITOR], [sitePageId(1), sitePageId(2)]),
743
+ );
744
+
745
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
746
+ expect(
747
+ audience.excludedStatusPages.find(
748
+ (excluded: { statusPageId: string }): boolean => {
749
+ return excluded.statusPageId === sitePageId(2);
750
+ },
751
+ ),
752
+ ).toEqual({
753
+ statusPageId: sitePageId(2),
754
+ name: "Site 02",
755
+ reason: IncidentSubscriberAudienceExclusionReason.HidesIncidents,
756
+ });
757
+ });
758
+
759
+ test("a selected page that lists none of the monitors is reported, and not notified", async () => {
760
+ useSitePages();
761
+
762
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
763
+ draft([SHARED_MONITOR], [sitePageId(1), LONELY_PAGE_ID]),
764
+ );
765
+
766
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
767
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([
768
+ { statusPageId: LONELY_PAGE_ID, name: "Lonely" },
769
+ ]);
770
+ });
771
+
772
+ test("no monitors: nobody, and no page is reported as not listing them", async () => {
773
+ useSitePages();
774
+
775
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
776
+ draft([], [sitePageId(1)]),
777
+ );
778
+
779
+ expect(audience.hasMonitors).toBe(false);
780
+ expect(audience.statusPages).toEqual([]);
781
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([]);
782
+ expect(IncidentSubscriberAudience.reachesAnyone(audience)).toBe(false);
783
+ expect(findByMonitors).not.toHaveBeenCalled();
784
+ });
785
+
786
+ test("ids are compared without regard to case or duplicates", async () => {
787
+ useSitePages();
788
+
789
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
790
+ draft(
791
+ [SHARED_MONITOR.toUpperCase(), SHARED_MONITOR],
792
+ [sitePageId(1).toUpperCase(), sitePageId(1)],
793
+ ),
794
+ );
795
+
796
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
797
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([]);
798
+ });
799
+ });
800
+
801
+ describe("no address ever leaves the server", () => {
802
+ test("subscribers are counted with the aggregate and never read", async () => {
803
+ useSitePages();
804
+
805
+ await audienceFor(draft([SHARED_MONITOR]));
806
+
807
+ expect(countActiveSubscribersByChannel).toHaveBeenCalledTimes(1);
808
+ expect(subscriberFindBy).not.toHaveBeenCalled();
809
+ expect(getSubscribersByStatusPage).not.toHaveBeenCalled();
810
+ });
811
+
812
+ test("the answer holds only page names, ids and numbers", async () => {
813
+ useSitePages();
814
+
815
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
816
+ const text: string = JSON.stringify(call.sent);
817
+
818
+ expect(call.thrown).toBeUndefined();
819
+ expect(text).not.toMatch(/@/);
820
+ expect(text).not.toMatch(/https?:/i);
821
+ expect(text).not.toMatch(
822
+ /subscriberEmail|subscriberPhone|subscriberWebhook|IncomingWebhookUrl/,
823
+ );
824
+
825
+ // Every key in the answer is one the audience type declares.
826
+ expect(Object.keys(call.sent!).sort()).toEqual(
827
+ [
828
+ "excludedStatusPages",
829
+ "hasMonitors",
830
+ "hiddenStatusPageCount",
831
+ "isHiddenFromStatusPages",
832
+ "isScoped",
833
+ "selectedStatusPagesNotListingMonitors",
834
+ "statusPages",
835
+ ].sort(),
836
+ );
837
+
838
+ for (const statusPage of call.sent!["statusPages"] as Array<JSONObject>) {
839
+ expect(Object.keys(statusPage).sort()).toEqual(
840
+ ["name", "statusPageId", "subscriberCounts"].sort(),
841
+ );
842
+
843
+ for (const value of Object.values(
844
+ statusPage["subscriberCounts"] as JSONObject,
845
+ )) {
846
+ expect(typeof value).toBe("number");
847
+ }
848
+ }
849
+ });
850
+ });
851
+
852
+ describe("status pages the caller cannot read", () => {
853
+ test("notified pages the caller cannot read collapse into a count, and are not counted", async () => {
854
+ useSitePages();
855
+ readablePageIds = [sitePageId(1), sitePageId(3)];
856
+
857
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
858
+ draft([SHARED_MONITOR]),
859
+ );
860
+
861
+ expect(names(audience.statusPages)).toEqual(["Site 01", "Site 03"]);
862
+ expect(audience.hiddenStatusPageCount).toBe(3);
863
+ expect(IncidentSubscriberAudience.reachesAnyone(audience)).toBe(true);
864
+
865
+ const countedIds: Array<string> = (
866
+ countActiveSubscribersByChannel.mock.calls[0]![0] as {
867
+ statusPageIds: Array<ObjectID>;
868
+ }
869
+ ).statusPageIds.map((id: ObjectID): string => {
870
+ return id.toString();
871
+ });
872
+
873
+ expect(countedIds).toEqual([sitePageId(1), sitePageId(3)]);
874
+
875
+ const text: string = JSON.stringify(
876
+ IncidentSubscriberAudience.toJSON(audience),
877
+ );
878
+
879
+ for (const hidden of [2, 4, 5]) {
880
+ expect(text).not.toContain(siteName(hidden));
881
+ expect(text).not.toContain(sitePageId(hidden));
882
+ }
883
+ });
884
+
885
+ test("the pages are read with the caller's own permissions", async () => {
886
+ useSitePages();
887
+
888
+ await audienceFor(draft([SHARED_MONITOR]));
889
+
890
+ const callerReads: Array<{ props: DatabaseCommonInteractionProps }> = (
891
+ StatusPageService.findBy as unknown as jest.Mock
892
+ ).mock.calls
893
+ .map((call: Array<unknown>) => {
894
+ return call[0] as { props: DatabaseCommonInteractionProps };
895
+ })
896
+ .filter((findBy: { props: DatabaseCommonInteractionProps }): boolean => {
897
+ return !findBy.props.isRoot;
898
+ });
899
+
900
+ expect(callerReads.length).toBeGreaterThan(0);
901
+ expect(callerReads[0]!.props).toBe(callerProps);
902
+ });
903
+
904
+ test("excluded and not-listing pages the caller cannot read are left out, never named", async () => {
905
+ useSitePages();
906
+ readablePageIds = [sitePageId(1)];
907
+
908
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
909
+ draft([SHARED_MONITOR], [sitePageId(1), LONELY_PAGE_ID]),
910
+ );
911
+
912
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
913
+ expect(audience.hiddenStatusPageCount).toBe(0);
914
+ expect(audience.excludedStatusPages).toEqual([]);
915
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([]);
916
+ });
917
+
918
+ test("a caller with no status page read access sees only a count", async () => {
919
+ useSitePages();
920
+ statusPageReadRefused = true;
921
+
922
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
923
+ draft([SHARED_MONITOR]),
924
+ );
925
+
926
+ expect(audience.statusPages).toEqual([]);
927
+ expect(audience.excludedStatusPages).toEqual([]);
928
+ expect(audience.hiddenStatusPageCount).toBe(5);
929
+ expect(countActiveSubscribersByChannel).not.toHaveBeenCalled();
930
+ });
931
+ });
932
+
933
+ describe("the project is enforced", () => {
934
+ test("another project's incident is not found", async () => {
935
+ useSitePages();
936
+ incidents = [
937
+ {
938
+ id: INCIDENT_ID,
939
+ projectId: OTHER_PROJECT_ID,
940
+ monitorIds: [SHARED_MONITOR],
941
+ isVisibleOnStatusPage: true,
942
+ isPrivate: false,
943
+ scopedStatusPageIds: null,
944
+ },
945
+ ];
946
+
947
+ const call: RouteCall = await post({ incidentId: INCIDENT_ID });
948
+
949
+ expect(call.thrown).toBeInstanceOf(NotFoundException);
950
+ expect(call.sent).toBeUndefined();
951
+ expect(findByMonitors).not.toHaveBeenCalled();
952
+ expect(countActiveSubscribersByChannel).not.toHaveBeenCalled();
953
+
954
+ // Looked up in the caller's project, with the caller's permissions.
955
+ const lookup: { query: JSONObject; props: DatabaseCommonInteractionProps } =
956
+ incidentFindOneBy.mock.calls[0]![0] as {
957
+ query: JSONObject;
958
+ props: DatabaseCommonInteractionProps;
959
+ };
960
+ expect(sameId(lookup.query["projectId"], PROJECT_ID)).toBe(true);
961
+ expect(lookup.props).toBe(callerProps);
962
+ });
963
+
964
+ test("another project's monitor is refused before anything is resolved", async () => {
965
+ useSitePages();
966
+ monitorProjects[LONELY_MONITOR] = OTHER_PROJECT_ID;
967
+
968
+ const call: RouteCall = await post(draft([SHARED_MONITOR, LONELY_MONITOR]));
969
+
970
+ expect(call.thrown).toBeInstanceOf(BadDataException);
971
+ expect((call.thrown as BadDataException).message).toBe(
972
+ "One or more of these monitors were not found in this project.",
973
+ );
974
+ expect(findByMonitors).not.toHaveBeenCalled();
975
+ expect(call.sent).toBeUndefined();
976
+ });
977
+
978
+ test("another project's status page is refused before anything is resolved", async () => {
979
+ useSitePages();
980
+ pages.push({
981
+ id: "b0000000-0000-4000-8000-000000000077",
982
+ name: "Their page",
983
+ projectId: OTHER_PROJECT_ID,
984
+ });
985
+
986
+ const call: RouteCall = await post(
987
+ draft([SHARED_MONITOR], ["b0000000-0000-4000-8000-000000000077"]),
988
+ );
989
+
990
+ expect(call.thrown).toBeInstanceOf(BadDataException);
991
+ expect((call.thrown as BadDataException).message).toBe(
992
+ "One or more of these status pages were not found in this project.",
993
+ );
994
+ expect(findByMonitors).not.toHaveBeenCalled();
995
+ });
996
+
997
+ test("a status page that does not exist is refused the same way", async () => {
998
+ useSitePages();
999
+
1000
+ const call: RouteCall = await post(
1001
+ draft([SHARED_MONITOR], ["b0000000-0000-4000-8000-000000000066"]),
1002
+ );
1003
+
1004
+ expect(call.thrown).toBeInstanceOf(BadDataException);
1005
+ });
1006
+
1007
+ test("another project's page reached through a resource is never reported", async () => {
1008
+ useSitePages();
1009
+ pages = pages.map((page: PageFixture): PageFixture => {
1010
+ return page.id === sitePageId(5)
1011
+ ? { ...page, projectId: OTHER_PROJECT_ID }
1012
+ : page;
1013
+ });
1014
+
1015
+ const audience: IncidentSubscriberAudienceResult = await audienceFor(
1016
+ draft([SHARED_MONITOR]),
1017
+ );
1018
+
1019
+ expect(names(audience.statusPages)).toEqual([
1020
+ "Site 01",
1021
+ "Site 02",
1022
+ "Site 03",
1023
+ "Site 04",
1024
+ ]);
1025
+ expect(audience.hiddenStatusPageCount).toBe(0);
1026
+ });
1027
+
1028
+ test("subscribers are counted in the caller's project", async () => {
1029
+ useSitePages();
1030
+
1031
+ await audienceFor(draft([SHARED_MONITOR]));
1032
+
1033
+ expect(
1034
+ sameId(
1035
+ (
1036
+ countActiveSubscribersByChannel.mock.calls[0]![0] as {
1037
+ projectId: ObjectID;
1038
+ }
1039
+ ).projectId,
1040
+ PROJECT_ID,
1041
+ ),
1042
+ ).toBe(true);
1043
+ });
1044
+ });
1045
+
1046
+ describe("an incident that exists", () => {
1047
+ function useIncident(overrides: Partial<IncidentFixture> = {}): void {
1048
+ incidents = [
1049
+ {
1050
+ id: INCIDENT_ID,
1051
+ projectId: PROJECT_ID,
1052
+ monitorIds: [SHARED_MONITOR],
1053
+ isVisibleOnStatusPage: true,
1054
+ isPrivate: false,
1055
+ scopedStatusPageIds: null,
1056
+ ...overrides,
1057
+ },
1058
+ ];
1059
+ }
1060
+
1061
+ test("reaches the pages its stored scope allows", async () => {
1062
+ useSitePages();
1063
+ useIncident({ scopedStatusPageIds: [sitePageId(2), sitePageId(4)] });
1064
+
1065
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1066
+ incidentId: INCIDENT_ID,
1067
+ });
1068
+
1069
+ expect(audience.isScoped).toBe(true);
1070
+ expect(audience.isHiddenFromStatusPages).toBe(false);
1071
+ expect(names(audience.statusPages)).toEqual(["Site 02", "Site 04"]);
1072
+ expect(audience.statusPages[1]!.subscriberCounts).toEqual(
1073
+ counts({ microsoftTeams: 4 }),
1074
+ );
1075
+ });
1076
+
1077
+ test("an unscoped incident reaches every page its monitors reach", async () => {
1078
+ useSitePages();
1079
+ useIncident();
1080
+
1081
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1082
+ incidentId: INCIDENT_ID,
1083
+ });
1084
+
1085
+ expect(audience.isScoped).toBe(false);
1086
+ expect(audience.statusPages).toHaveLength(5);
1087
+ });
1088
+
1089
+ test("a scope whose pages were all deleted reaches nothing", async () => {
1090
+ useSitePages();
1091
+ useIncident({ scopedStatusPageIds: [] });
1092
+
1093
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1094
+ incidentId: INCIDENT_ID,
1095
+ });
1096
+
1097
+ expect(audience.isScoped).toBe(true);
1098
+ expect(audience.statusPages).toEqual([]);
1099
+ expect(IncidentSubscriberAudience.reachesAnyone(audience)).toBe(false);
1100
+ });
1101
+
1102
+ test("a hidden incident is reported as hidden: nothing will be sent", async () => {
1103
+ useSitePages();
1104
+ useIncident({ isVisibleOnStatusPage: false });
1105
+
1106
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1107
+ incidentId: INCIDENT_ID,
1108
+ });
1109
+
1110
+ expect(audience.isHiddenFromStatusPages).toBe(true);
1111
+ expect(IncidentSubscriberAudience.reachesAnyone(audience)).toBe(false);
1112
+ });
1113
+
1114
+ test("a private incident is reported as hidden", async () => {
1115
+ useSitePages();
1116
+ useIncident({ isPrivate: true });
1117
+
1118
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1119
+ incidentId: INCIDENT_ID,
1120
+ });
1121
+
1122
+ expect(audience.isHiddenFromStatusPages).toBe(true);
1123
+ });
1124
+
1125
+ test("a scoped page that lists none of its monitors is reported", async () => {
1126
+ useSitePages();
1127
+ useIncident({ scopedStatusPageIds: [sitePageId(1), LONELY_PAGE_ID] });
1128
+
1129
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1130
+ incidentId: INCIDENT_ID,
1131
+ });
1132
+
1133
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
1134
+ expect(audience.selectedStatusPagesNotListingMonitors).toEqual([
1135
+ { statusPageId: LONELY_PAGE_ID, name: "Lonely" },
1136
+ ]);
1137
+ });
1138
+
1139
+ test("an incident the caller cannot read is not found", async () => {
1140
+ useSitePages();
1141
+ useIncident();
1142
+ incidentFindOneBy.mockImplementation((() => {
1143
+ return Promise.resolve(null);
1144
+ }) as never);
1145
+
1146
+ const call: RouteCall = await post({ incidentId: INCIDENT_ID });
1147
+
1148
+ expect(call.thrown).toBeInstanceOf(NotFoundException);
1149
+ });
1150
+
1151
+ /*
1152
+ * A Retry of the 'created' notification resumes after the pages already
1153
+ * sent it in full, so its confirmation must not list them as notified.
1154
+ */
1155
+ describe("who a Retry of its 'created' notification reaches", () => {
1156
+ test("the pages already sent it in full are left out, with why", async () => {
1157
+ useSitePages();
1158
+ useIncident({
1159
+ statusPagesNotifiedOnCreation: [sitePageId(1), sitePageId(3)],
1160
+ });
1161
+
1162
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1163
+ incidentId: INCIDENT_ID,
1164
+ excludeStatusPagesNotifiedOnCreation: true,
1165
+ });
1166
+
1167
+ expect(names(audience.statusPages)).toEqual([
1168
+ "Site 02",
1169
+ "Site 04",
1170
+ "Site 05",
1171
+ ]);
1172
+ expect(
1173
+ audience.excludedStatusPages.filter(
1174
+ (excluded: { reason: IncidentSubscriberAudienceExclusionReason }) => {
1175
+ return (
1176
+ excluded.reason ===
1177
+ IncidentSubscriberAudienceExclusionReason.AlreadyNotified
1178
+ );
1179
+ },
1180
+ ),
1181
+ ).toEqual([
1182
+ {
1183
+ statusPageId: sitePageId(1),
1184
+ name: "Site 01",
1185
+ reason: IncidentSubscriberAudienceExclusionReason.AlreadyNotified,
1186
+ },
1187
+ {
1188
+ statusPageId: sitePageId(3),
1189
+ name: "Site 03",
1190
+ reason: IncidentSubscriberAudienceExclusionReason.AlreadyNotified,
1191
+ },
1192
+ ]);
1193
+ });
1194
+
1195
+ test("without the request every page it reaches is listed, as for Resend", async () => {
1196
+ useSitePages();
1197
+ useIncident({
1198
+ statusPagesNotifiedOnCreation: [sitePageId(1), sitePageId(3)],
1199
+ });
1200
+
1201
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1202
+ incidentId: INCIDENT_ID,
1203
+ });
1204
+
1205
+ expect(audience.statusPages).toHaveLength(5);
1206
+ expect(audience.excludedStatusPages).toEqual([]);
1207
+ });
1208
+
1209
+ test("only a real yes asks for it", async () => {
1210
+ useSitePages();
1211
+ useIncident({ statusPagesNotifiedOnCreation: [sitePageId(1)] });
1212
+
1213
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1214
+ incidentId: INCIDENT_ID,
1215
+ excludeStatusPagesNotifiedOnCreation: "true",
1216
+ });
1217
+
1218
+ expect(audience.statusPages).toHaveLength(5);
1219
+ });
1220
+
1221
+ test("an incident with no record yet leaves nothing out", async () => {
1222
+ useSitePages();
1223
+ useIncident();
1224
+
1225
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1226
+ incidentId: INCIDENT_ID,
1227
+ excludeStatusPagesNotifiedOnCreation: true,
1228
+ });
1229
+
1230
+ expect(audience.statusPages).toHaveLength(5);
1231
+ });
1232
+
1233
+ test("a page already sent it that the caller cannot read is not named", async () => {
1234
+ useSitePages();
1235
+ useIncident({
1236
+ statusPagesNotifiedOnCreation: [sitePageId(1), sitePageId(3)],
1237
+ });
1238
+ readablePageIds = [sitePageId(2), sitePageId(3), sitePageId(4)];
1239
+
1240
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1241
+ incidentId: INCIDENT_ID,
1242
+ excludeStatusPagesNotifiedOnCreation: true,
1243
+ });
1244
+
1245
+ expect(
1246
+ audience.excludedStatusPages.map((excluded: { name: string }) => {
1247
+ return excluded.name;
1248
+ }),
1249
+ ).toEqual(["Site 03"]);
1250
+ // Site 05 will be sent it and cannot be read; Site 01 will not be.
1251
+ expect(audience.hiddenStatusPageCount).toBe(1);
1252
+ });
1253
+ });
1254
+
1255
+ test("an incident on no monitor reaches nobody", async () => {
1256
+ useSitePages();
1257
+ useIncident({ monitorIds: [] });
1258
+
1259
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1260
+ incidentId: INCIDENT_ID,
1261
+ });
1262
+
1263
+ expect(audience.hasMonitors).toBe(false);
1264
+ expect(audience.statusPages).toEqual([]);
1265
+ });
1266
+ });
1267
+
1268
+ describe("permissions", () => {
1269
+ test.each([
1270
+ Permission.ProjectOwner,
1271
+ Permission.ProjectAdmin,
1272
+ Permission.ProjectMember,
1273
+ Permission.IncidentAdmin,
1274
+ Permission.IncidentMember,
1275
+ Permission.CreateProjectIncident,
1276
+ Permission.EditProjectIncident,
1277
+ Permission.CreateIncidentPublicNote,
1278
+ ])("%s may ask", async (permission: Permission) => {
1279
+ useSitePages();
1280
+ callerProps = permissionProps([permission]);
1281
+
1282
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
1283
+
1284
+ expect(call.thrown).toBeUndefined();
1285
+ expect(call.sent).toBeDefined();
1286
+ });
1287
+
1288
+ test.each([
1289
+ Permission.IncidentViewer,
1290
+ Permission.ReadProjectIncident,
1291
+ Permission.StatusPageViewer,
1292
+ Permission.Viewer,
1293
+ ])("%s alone may not", async (permission: Permission) => {
1294
+ useSitePages();
1295
+ callerProps = permissionProps([permission]);
1296
+
1297
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
1298
+
1299
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
1300
+ expect(findByMonitors).not.toHaveBeenCalled();
1301
+ expect(countActiveSubscribersByChannel).not.toHaveBeenCalled();
1302
+ });
1303
+
1304
+ test("a blocked permission is not a grant", async () => {
1305
+ useSitePages();
1306
+ callerProps = permissionProps([Permission.IncidentViewer], {
1307
+ blocked: [Permission.IncidentMember],
1308
+ });
1309
+
1310
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
1311
+
1312
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
1313
+ });
1314
+
1315
+ test("a master admin may ask", async () => {
1316
+ useSitePages();
1317
+ callerProps = {
1318
+ ...permissionProps([]),
1319
+ isMasterAdmin: true,
1320
+ };
1321
+
1322
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
1323
+
1324
+ expect(call.thrown).toBeUndefined();
1325
+ });
1326
+
1327
+ test("a request without a project is refused", async () => {
1328
+ callerProps = {
1329
+ userId: USER_ID,
1330
+ };
1331
+
1332
+ const call: RouteCall = await post(draft([SHARED_MONITOR]));
1333
+
1334
+ expect(call.thrown).toBeInstanceOf(BadDataException);
1335
+ expect(findByMonitors).not.toHaveBeenCalled();
1336
+ });
1337
+ });
1338
+
1339
+ describe("the request", () => {
1340
+ test.each([
1341
+ [
1342
+ "both an incident and a draft",
1343
+ { incidentId: INCIDENT_ID, monitorIds: [SHARED_MONITOR] },
1344
+ "Send either incidentId, or monitorIds and statusPageIds - not both.",
1345
+ ],
1346
+ ["neither", {}, "Send incidentId, or monitorIds and statusPageIds."],
1347
+ [
1348
+ "an invalid incident id",
1349
+ { incidentId: "not-an-id" },
1350
+ "incidentId must be a valid ID.",
1351
+ ],
1352
+ [
1353
+ "an invalid monitor id",
1354
+ { monitorIds: [SHARED_MONITOR, "nope"] },
1355
+ "monitorIds must be a valid ID.",
1356
+ ],
1357
+ [
1358
+ "monitor ids that are not a list",
1359
+ { monitorIds: SHARED_MONITOR },
1360
+ "monitorIds must be a list of IDs.",
1361
+ ],
1362
+ [
1363
+ "status page ids that are not a list",
1364
+ { monitorIds: [SHARED_MONITOR], statusPageIds: "x" },
1365
+ "statusPageIds must be a list of IDs.",
1366
+ ],
1367
+ ])("refuses %s", async (_name: string, body: JSONObject, message: string) => {
1368
+ const call: RouteCall = await post(body);
1369
+
1370
+ expect(call.thrown).toBeInstanceOf(BadDataException);
1371
+ expect((call.thrown as BadDataException).message).toBe(message);
1372
+ });
1373
+
1374
+ test("refuses more ids than one request may name", async () => {
1375
+ const tooMany: Array<string> = Array.from(
1376
+ { length: IncidentSubscriberAudience.maxIdsPerRequest + 1 },
1377
+ (): string => {
1378
+ return ObjectID.generate().toString();
1379
+ },
1380
+ );
1381
+
1382
+ const call: RouteCall = await post({ monitorIds: tooMany });
1383
+
1384
+ expect(call.thrown).toBeInstanceOf(BadDataException);
1385
+ expect(findByMonitors).not.toHaveBeenCalled();
1386
+ });
1387
+
1388
+ test("accepts serialized ObjectIDs", async () => {
1389
+ useSitePages();
1390
+
1391
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1392
+ monitorIds: [new ObjectID(SHARED_MONITOR).toJSON()],
1393
+ statusPageIds: [new ObjectID(sitePageId(1)).toJSON()],
1394
+ });
1395
+
1396
+ expect(names(audience.statusPages)).toEqual(["Site 01"]);
1397
+ });
1398
+
1399
+ test("status page ids alone are a draft on no monitor", async () => {
1400
+ useSitePages();
1401
+
1402
+ const audience: IncidentSubscriberAudienceResult = await audienceFor({
1403
+ statusPageIds: [sitePageId(1)],
1404
+ });
1405
+
1406
+ expect(audience.hasMonitors).toBe(false);
1407
+ });
1408
+ });