@oneuptime/common 14.0.6 → 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 (556) 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/InventoryItem.ts +23 -12
  6. package/Models/DatabaseModels/StatusPage.ts +55 -0
  7. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  8. package/Server/API/IncidentAPI.ts +162 -0
  9. package/Server/API/StatusPageAPI.ts +487 -171
  10. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  20. package/Server/Infrastructure/Semaphore.ts +10 -0
  21. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  22. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  23. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  24. package/Server/Services/AlertService.ts +32 -36
  25. package/Server/Services/DatabaseService.ts +93 -10
  26. package/Server/Services/IncidentAlertService.ts +609 -2
  27. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  28. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  29. package/Server/Services/IncidentEpisodeService.ts +15 -0
  30. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  31. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  32. package/Server/Services/IncidentService.ts +1663 -103
  33. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  34. package/Server/Services/IncidentTemplateService.ts +109 -0
  35. package/Server/Services/InventoryItemService.ts +38 -0
  36. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  37. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  38. package/Server/Services/ScheduledMaintenanceService.ts +118 -64
  39. package/Server/Services/SmsService.ts +10 -0
  40. package/Server/Services/StatusPageService.ts +65 -8
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  42. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  43. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  44. package/Server/Services/UserNotificationRuleService.ts +55 -53
  45. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  46. package/Server/Types/Markdown.ts +153 -6
  47. package/Server/Types/Workflow/Components/Email.ts +61 -2
  48. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  49. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  50. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  51. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  52. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  53. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  54. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  55. package/Server/Utils/Response.ts +31 -0
  56. package/Server/Utils/SSRFProtection.ts +76 -16
  57. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  58. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  59. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  60. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  61. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  62. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  63. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  64. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  65. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  66. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  67. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  68. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  69. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  70. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  71. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  72. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  73. package/Server/Utils/StatusPageResource.ts +51 -20
  74. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  75. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  76. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  77. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  78. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  79. package/Server/Utils/VM/VMAPI.ts +1 -0
  80. package/Server/Utils/VM/VMRunner.ts +10 -0
  81. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  82. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  83. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  84. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  85. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  86. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  87. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  88. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  89. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  90. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  91. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  92. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  93. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  94. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  95. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  96. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  97. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  98. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  99. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  100. package/Tests/App/Dashboard/EventNotes.test.tsx +776 -0
  101. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  102. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  103. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +534 -6
  104. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  105. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  106. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  107. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  108. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  109. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  110. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  111. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  112. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  113. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  114. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  115. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  116. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  117. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  118. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  119. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  120. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  121. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  122. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  123. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  124. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  125. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  126. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  127. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  128. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  129. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  130. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  131. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  132. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  133. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  134. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  135. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  136. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  137. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  138. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  139. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  140. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  141. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  142. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  143. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  144. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  145. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  146. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  147. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  148. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  149. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  150. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  151. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  152. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  153. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  154. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  155. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  156. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  157. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  158. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  159. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  160. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  161. package/Tests/Server/Services/DomainService.test.ts +525 -0
  162. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  163. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  164. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  165. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  166. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  167. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  168. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  169. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  170. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  171. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  172. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  173. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  174. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  175. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  176. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  177. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  178. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  179. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  180. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  181. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  182. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  183. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  184. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  185. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  186. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  187. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  188. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  189. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  190. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  191. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  192. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  193. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  194. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  195. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  196. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  197. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  198. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  199. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  200. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  201. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  202. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  203. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  204. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  205. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  206. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  207. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  208. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  209. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  210. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  211. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  212. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  213. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  214. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  215. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  216. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  217. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  218. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  219. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  220. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  221. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  222. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  223. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  224. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  225. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  226. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  227. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  228. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  229. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  230. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  231. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  232. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  233. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  234. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  235. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  236. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  237. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  238. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  239. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  240. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  241. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  242. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  243. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  244. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  245. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  246. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  247. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  248. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  249. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  250. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  251. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  252. package/Tests/Types/SafeHtml.test.ts +102 -0
  253. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  254. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  255. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  256. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  257. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  258. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  259. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  260. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  261. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  262. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  263. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  264. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  265. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  266. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  267. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  268. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  269. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  270. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  271. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  272. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  273. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  274. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  275. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  276. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  277. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  278. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  279. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  280. package/Types/CustomField/CustomFieldType.ts +14 -0
  281. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  282. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  283. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  284. package/Types/Incident/IncidentAlertLink.ts +19 -0
  285. package/Types/SafeHtml.ts +93 -0
  286. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  287. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  288. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  289. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  290. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  291. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  292. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  293. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  294. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  295. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  296. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  297. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  298. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  299. package/Types/Topology/TopologyApi.ts +428 -0
  300. package/Types/Topology/TopologyTypeRules.ts +191 -0
  301. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  302. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  303. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  304. package/UI/Components/Forms/ModelForm.tsx +70 -8
  305. package/UI/Components/Forms/Validation.ts +1 -1
  306. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  307. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  308. package/UI/Components/ModelTable/BaseModelTable.tsx +9 -8
  309. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  310. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  311. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  312. package/Utils/API.ts +247 -2
  313. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  314. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  315. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  316. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  317. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  318. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  319. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  320. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  321. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  322. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  323. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  324. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  325. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  326. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  327. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  328. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  329. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  330. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  331. package/build/dist/Server/API/IncidentAPI.js +100 -1
  332. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  333. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  334. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  335. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  336. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  337. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  338. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  339. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  340. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  341. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  342. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  343. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  344. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  345. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  346. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  347. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  348. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  349. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  350. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  351. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  352. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  353. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  354. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  355. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  356. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  357. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  358. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  359. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  360. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  361. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  362. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  363. package/build/dist/Server/Services/AlertService.js +21 -33
  364. package/build/dist/Server/Services/AlertService.js.map +1 -1
  365. package/build/dist/Server/Services/DatabaseService.js +56 -7
  366. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  367. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  368. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  369. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  370. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  371. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  372. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  373. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  374. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  375. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  376. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  377. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  378. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  379. package/build/dist/Server/Services/IncidentService.js +1148 -42
  380. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  381. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  382. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  383. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  384. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  385. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  386. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  387. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  388. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  389. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  390. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  391. package/build/dist/Server/Services/ScheduledMaintenanceService.js +84 -57
  392. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  393. package/build/dist/Server/Services/SmsService.js +2 -22
  394. package/build/dist/Server/Services/SmsService.js.map +1 -1
  395. package/build/dist/Server/Services/StatusPageService.js +47 -9
  396. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  397. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  398. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  399. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  400. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  401. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  402. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  403. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  404. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  405. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  406. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  407. package/build/dist/Server/Types/Markdown.js +110 -6
  408. package/build/dist/Server/Types/Markdown.js.map +1 -1
  409. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  410. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  411. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  412. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  413. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  414. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  415. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  416. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  417. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  418. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  419. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  420. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  421. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  422. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  425. package/build/dist/Server/Utils/Response.js +30 -0
  426. package/build/dist/Server/Utils/Response.js.map +1 -1
  427. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  428. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  429. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  430. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  431. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  432. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  433. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  434. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  435. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  436. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  437. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  438. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  439. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  440. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  441. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  442. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  443. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  444. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  445. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  446. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  447. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  448. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  449. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  450. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  451. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  452. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  453. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  454. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  455. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  456. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  457. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  458. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  459. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  460. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  461. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  462. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  463. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  464. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  465. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  466. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  467. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  468. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  469. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  470. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  471. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  472. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  473. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  474. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  475. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  476. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  477. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  478. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  479. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  480. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  481. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  482. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  483. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  484. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  485. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  486. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  487. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  488. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  489. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  490. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  491. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  492. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  493. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  494. package/build/dist/Types/SafeHtml.js +84 -0
  495. package/build/dist/Types/SafeHtml.js.map +1 -0
  496. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  497. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  498. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  499. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  500. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  501. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  502. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  503. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  504. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  505. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  506. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  507. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  508. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  509. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  510. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  511. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  512. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  513. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  514. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  515. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  516. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  517. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  518. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  519. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  520. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  521. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  522. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  523. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  524. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  525. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  526. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  527. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  528. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  529. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  530. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  531. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  532. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  533. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  534. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  535. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  536. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  537. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  538. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +2 -2
  539. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  540. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  541. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  542. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  543. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  544. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  545. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  546. package/build/dist/Utils/API.js +177 -1
  547. package/build/dist/Utils/API.js.map +1 -1
  548. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  549. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  550. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  551. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  552. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  553. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  554. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  555. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  556. package/package.json +1 -1
@@ -0,0 +1,1868 @@
1
+ import File from "../../../Models/DatabaseModels/File";
2
+ import Incident from "../../../Models/DatabaseModels/Incident";
3
+ import IncidentEpisode from "../../../Models/DatabaseModels/IncidentEpisode";
4
+ import IncidentEpisodeMember from "../../../Models/DatabaseModels/IncidentEpisodeMember";
5
+ import IncidentEpisodePublicNote from "../../../Models/DatabaseModels/IncidentEpisodePublicNote";
6
+ import IncidentPublicNote from "../../../Models/DatabaseModels/IncidentPublicNote";
7
+ import IncidentState from "../../../Models/DatabaseModels/IncidentState";
8
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
9
+ import StatusPage from "../../../Models/DatabaseModels/StatusPage";
10
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
11
+ import StatusPageAPI from "../../../Server/API/StatusPageAPI";
12
+ import IncidentEpisodeMemberService from "../../../Server/Services/IncidentEpisodeMemberService";
13
+ import IncidentEpisodePublicNoteService from "../../../Server/Services/IncidentEpisodePublicNoteService";
14
+ import IncidentEpisodeService from "../../../Server/Services/IncidentEpisodeService";
15
+ import IncidentEpisodeStateTimelineService from "../../../Server/Services/IncidentEpisodeStateTimelineService";
16
+ import IncidentPublicNoteService from "../../../Server/Services/IncidentPublicNoteService";
17
+ import IncidentService from "../../../Server/Services/IncidentService";
18
+ import IncidentStateService from "../../../Server/Services/IncidentStateService";
19
+ import IncidentStateTimelineService from "../../../Server/Services/IncidentStateTimelineService";
20
+ import MonitorGroupService from "../../../Server/Services/MonitorGroupService";
21
+ import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
22
+ import StatusPageGroupService from "../../../Server/Services/StatusPageGroupService";
23
+ import StatusPageHistoryChartBarColorRuleService from "../../../Server/Services/StatusPageHistoryChartBarColorRuleService";
24
+ import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
25
+ import StatusPageService from "../../../Server/Services/StatusPageService";
26
+ import {
27
+ ExpressRequest,
28
+ ExpressResponse,
29
+ NextFunction,
30
+ } from "../../../Server/Utils/Express";
31
+ import Response from "../../../Server/Utils/Response";
32
+ import { INCIDENT_SCOPE_COLUMNS } from "../../../Server/Utils/StatusPage/IncidentStatusPageScope";
33
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
34
+ import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
35
+ import Dictionary from "../../../Types/Dictionary";
36
+ import NotFoundException from "../../../Types/Exception/NotFoundException";
37
+ import { JSONArray, JSONObject } from "../../../Types/JSON";
38
+ import ObjectID from "../../../Types/ObjectID";
39
+ import PositiveNumber from "../../../Types/PositiveNumber";
40
+ import { mockRouter } from "./Helpers";
41
+ import {
42
+ afterEach,
43
+ beforeAll,
44
+ beforeEach,
45
+ describe,
46
+ expect,
47
+ it,
48
+ jest,
49
+ } from "@jest/globals";
50
+
51
+ /*
52
+ * What a public status page shows of an incident limited to some status pages
53
+ * (Incident.statusPages), and of the incidents a page that only shows those
54
+ * (StatusPage.onlyShowScopedIncidents) leaves out - through every endpoint:
55
+ * the overview (active incidents, the uptime bar's timeline incidents and
56
+ * active episodes), the incident list and detail, the episode list and
57
+ * detail, and the attachment downloads.
58
+ *
59
+ * The services behind StatusPageAPI are replaced by a small in-memory
60
+ * database that answers each incident query the way Postgres would: the
61
+ * page's monitors (the IncidentMonitor join), the scope split
62
+ * (isScopedToStatusPages, and the IncidentStatusPage join for the scoped
63
+ * half), visibility, project and ids - and returns only the columns a query
64
+ * selects, as a real read does. So a code path that forgets to route through
65
+ * IncidentStatusPageScope, or to select the page's onlyShowScopedIncidents,
66
+ * shows up here as the wrong incidents on a page.
67
+ *
68
+ * The pages: Site A and Site B list the shared and the second monitor; Site
69
+ * C lists the shared monitor and only shows incidents limited to it; Site D
70
+ * lists nothing.
71
+ */
72
+
73
+ // Avoid the unrelated PasswordHash TS5.9 Buffer/BinaryLike compile failure.
74
+ jest.mock("../../../Server/Utils/PasswordHash", () => {
75
+ return {
76
+ __esModule: true,
77
+ default: class PasswordHashStub {},
78
+ };
79
+ });
80
+
81
+ jest.mock("../../../Server/Utils/Express", () => {
82
+ return {
83
+ getRouter: () => {
84
+ return mockRouter;
85
+ },
86
+ };
87
+ });
88
+
89
+ jest.mock("../../../Server/Utils/Response", () => {
90
+ return {
91
+ sendEntityArrayResponse: jest.fn(),
92
+ sendJsonObjectResponse: jest.fn(),
93
+ sendEmptySuccessResponse: jest.fn(),
94
+ sendEntityResponse: jest.fn(),
95
+ sendErrorResponse: jest.fn(),
96
+ sendFileResponse: jest.fn(),
97
+ setNoCacheHeaders: jest.fn(),
98
+ };
99
+ });
100
+
101
+ const OVERVIEW_ROUTE: string = "/status-page/overview/:statusPageIdOrDomain";
102
+ const INCIDENTS_ROUTE: string = "/status-page/incidents/:statusPageIdOrDomain";
103
+ const INCIDENT_DETAIL_ROUTE: string = `${INCIDENTS_ROUTE}/:incidentId`;
104
+ const EPISODES_ROUTE: string = "/status-page/episodes/:statusPageIdOrDomain";
105
+ const EPISODE_DETAIL_ROUTE: string = `${EPISODES_ROUTE}/:episodeId`;
106
+ const POSTMORTEM_ATTACHMENT_ROUTE: string =
107
+ "/status-page/incident/postmortem/attachment/:statusPageId/:incidentId/:fileId";
108
+ const INCIDENT_NOTE_ATTACHMENT_ROUTE: string =
109
+ "/status-page/incident-public-note/attachment/:statusPageId/:incidentId/:noteId/:fileId";
110
+ const EPISODE_NOTE_ATTACHMENT_ROUTE: string =
111
+ "/status-page/incident-episode-public-note/attachment/:statusPageId/:episodeId/:noteId/:fileId";
112
+
113
+ const PROJECT_ID: ObjectID = new ObjectID(
114
+ "10000000-0000-4000-8000-000000000001",
115
+ );
116
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
117
+ "10000000-0000-4000-8000-000000000002",
118
+ );
119
+
120
+ const UNRESOLVED_STATE_ID: string = "20000000-0000-4000-8000-000000000001";
121
+
122
+ // The pages, by what they are.
123
+ const SITE_A: string = "b0000000-0000-4000-8000-00000000000a";
124
+ const SITE_B: string = "b0000000-0000-4000-8000-00000000000b";
125
+ // Only shows incidents limited to it.
126
+ const SITE_C: string = "b0000000-0000-4000-8000-00000000000c";
127
+ // Lists no monitor.
128
+ const SITE_D: string = "b0000000-0000-4000-8000-00000000000d";
129
+ // A status page that was deleted; an incident limited to it keeps the flag.
130
+ const DELETED_SITE: string = "b0000000-0000-4000-8000-0000000000ff";
131
+
132
+ const ALL_SITES: Array<string> = [SITE_A, SITE_B, SITE_C, SITE_D];
133
+
134
+ const SHARED_MONITOR: string = "c0000000-0000-4000-8000-000000000001";
135
+ const SECOND_MONITOR: string = "c0000000-0000-4000-8000-000000000002";
136
+
137
+ // The incidents, by where they are limited to.
138
+ const UNSCOPED: string = "a0000000-0000-4000-8000-000000000001";
139
+ const SCOPED_TO_A: string = "a0000000-0000-4000-8000-000000000002";
140
+ const SCOPED_TO_A_AND_C: string = "a0000000-0000-4000-8000-000000000003";
141
+ const SCOPED_TO_DELETED_PAGE: string = "a0000000-0000-4000-8000-000000000004";
142
+ const HIDDEN_SCOPED_TO_B: string = "a0000000-0000-4000-8000-000000000005";
143
+ // On the second monitor, limited to Site A: a member of a mixed episode.
144
+ const SECOND_SCOPED_TO_A: string = "a0000000-0000-4000-8000-000000000006";
145
+ const OTHER_PROJECT_INCIDENT: string = "a0000000-0000-4000-8000-000000000007";
146
+
147
+ const ALL_INCIDENTS: Array<string> = [
148
+ UNSCOPED,
149
+ SCOPED_TO_A,
150
+ SCOPED_TO_A_AND_C,
151
+ SCOPED_TO_DELETED_PAGE,
152
+ HIDDEN_SCOPED_TO_B,
153
+ SECOND_SCOPED_TO_A,
154
+ OTHER_PROJECT_INCIDENT,
155
+ ];
156
+
157
+ // The episodes, by their member incidents.
158
+ const EPISODE_SCOPED_TO_A: string = "e0000000-0000-4000-8000-000000000001";
159
+ const EPISODE_MIXED: string = "e0000000-0000-4000-8000-000000000002";
160
+ const EPISODE_SCOPED_TO_A_AND_C: string =
161
+ "e0000000-0000-4000-8000-000000000003";
162
+ const EPISODE_NOWHERE: string = "e0000000-0000-4000-8000-000000000004";
163
+
164
+ const ALL_EPISODES: Array<string> = [
165
+ EPISODE_SCOPED_TO_A,
166
+ EPISODE_MIXED,
167
+ EPISODE_SCOPED_TO_A_AND_C,
168
+ EPISODE_NOWHERE,
169
+ ];
170
+
171
+ /*
172
+ * What each page shows. The same on every incident view (overview, list,
173
+ * detail, timeline): every incident here is unresolved and recent.
174
+ */
175
+ const INCIDENTS_SHOWN: Dictionary<Array<string>> = {
176
+ [SITE_A]: [UNSCOPED, SCOPED_TO_A, SCOPED_TO_A_AND_C, SECOND_SCOPED_TO_A],
177
+ [SITE_B]: [UNSCOPED],
178
+ [SITE_C]: [SCOPED_TO_A_AND_C],
179
+ [SITE_D]: [],
180
+ };
181
+
182
+ const EPISODES_SHOWN: Dictionary<Array<string>> = {
183
+ [SITE_A]: [EPISODE_SCOPED_TO_A, EPISODE_MIXED, EPISODE_SCOPED_TO_A_AND_C],
184
+ [SITE_B]: [EPISODE_MIXED],
185
+ [SITE_C]: [EPISODE_SCOPED_TO_A_AND_C],
186
+ [SITE_D]: [],
187
+ };
188
+
189
+ interface PageFixture {
190
+ id: string;
191
+ name: string;
192
+ onlyShowScopedIncidents: boolean;
193
+ monitorIds: Array<string>;
194
+ }
195
+
196
+ interface IncidentFixture {
197
+ id: string;
198
+ title: string;
199
+ projectId: ObjectID;
200
+ monitorIds: Array<string>;
201
+ // null: not limited to any status page.
202
+ scopedTo: Array<string> | null;
203
+ isVisibleOnStatusPage: boolean;
204
+ createdAt: Date;
205
+ }
206
+
207
+ interface EpisodeFixture {
208
+ id: string;
209
+ title: string;
210
+ memberIncidentIds: Array<string>;
211
+ }
212
+
213
+ const PAGES: Array<PageFixture> = [
214
+ {
215
+ id: SITE_A,
216
+ name: "Site A",
217
+ onlyShowScopedIncidents: false,
218
+ monitorIds: [SHARED_MONITOR, SECOND_MONITOR],
219
+ },
220
+ {
221
+ id: SITE_B,
222
+ name: "Site B",
223
+ onlyShowScopedIncidents: false,
224
+ monitorIds: [SHARED_MONITOR, SECOND_MONITOR],
225
+ },
226
+ {
227
+ id: SITE_C,
228
+ name: "Site C",
229
+ onlyShowScopedIncidents: true,
230
+ monitorIds: [SHARED_MONITOR],
231
+ },
232
+ {
233
+ id: SITE_D,
234
+ name: "Site D",
235
+ onlyShowScopedIncidents: false,
236
+ monitorIds: [],
237
+ },
238
+ ];
239
+
240
+ function hoursAgo(hours: number): Date {
241
+ return new Date(Date.now() - hours * 60 * 60 * 1000);
242
+ }
243
+
244
+ const INCIDENTS: Array<IncidentFixture> = [
245
+ {
246
+ id: UNSCOPED,
247
+ title: "Checkout is down everywhere",
248
+ projectId: PROJECT_ID,
249
+ monitorIds: [SHARED_MONITOR],
250
+ scopedTo: null,
251
+ isVisibleOnStatusPage: true,
252
+ createdAt: hoursAgo(7),
253
+ },
254
+ {
255
+ id: SCOPED_TO_A,
256
+ title: "Checkout is down at Site A",
257
+ projectId: PROJECT_ID,
258
+ monitorIds: [SHARED_MONITOR],
259
+ scopedTo: [SITE_A],
260
+ isVisibleOnStatusPage: true,
261
+ createdAt: hoursAgo(6),
262
+ },
263
+ {
264
+ id: SCOPED_TO_A_AND_C,
265
+ title: "Checkout is down at Sites A and C",
266
+ projectId: PROJECT_ID,
267
+ monitorIds: [SHARED_MONITOR],
268
+ scopedTo: [SITE_A, SITE_C],
269
+ isVisibleOnStatusPage: true,
270
+ createdAt: hoursAgo(5),
271
+ },
272
+ {
273
+ // Its only page was deleted; the join row went with it.
274
+ id: SCOPED_TO_DELETED_PAGE,
275
+ title: "Checkout is down at a closed site",
276
+ projectId: PROJECT_ID,
277
+ monitorIds: [SHARED_MONITOR],
278
+ scopedTo: [],
279
+ isVisibleOnStatusPage: true,
280
+ createdAt: hoursAgo(4),
281
+ },
282
+ {
283
+ id: HIDDEN_SCOPED_TO_B,
284
+ title: "Checkout is down at Site B, not published",
285
+ projectId: PROJECT_ID,
286
+ monitorIds: [SHARED_MONITOR],
287
+ scopedTo: [SITE_B],
288
+ isVisibleOnStatusPage: false,
289
+ createdAt: hoursAgo(3),
290
+ },
291
+ {
292
+ id: SECOND_SCOPED_TO_A,
293
+ title: "Payments are slow at Site A",
294
+ projectId: PROJECT_ID,
295
+ monitorIds: [SECOND_MONITOR],
296
+ scopedTo: [SITE_A],
297
+ isVisibleOnStatusPage: true,
298
+ createdAt: hoursAgo(2),
299
+ },
300
+ {
301
+ id: OTHER_PROJECT_INCIDENT,
302
+ title: "Another project's outage",
303
+ projectId: OTHER_PROJECT_ID,
304
+ monitorIds: [SHARED_MONITOR],
305
+ scopedTo: null,
306
+ isVisibleOnStatusPage: true,
307
+ createdAt: hoursAgo(1),
308
+ },
309
+ ];
310
+
311
+ const EPISODES: Array<EpisodeFixture> = [
312
+ {
313
+ id: EPISODE_SCOPED_TO_A,
314
+ title: "Site A checkout episode",
315
+ memberIncidentIds: [SCOPED_TO_A],
316
+ },
317
+ {
318
+ id: EPISODE_MIXED,
319
+ title: "Checkout and payments episode",
320
+ memberIncidentIds: [UNSCOPED, SECOND_SCOPED_TO_A],
321
+ },
322
+ {
323
+ id: EPISODE_SCOPED_TO_A_AND_C,
324
+ title: "Sites A and C checkout episode",
325
+ memberIncidentIds: [SCOPED_TO_A_AND_C],
326
+ },
327
+ {
328
+ id: EPISODE_NOWHERE,
329
+ title: "Closed site episode",
330
+ memberIncidentIds: [SCOPED_TO_DELETED_PAGE],
331
+ },
332
+ ];
333
+
334
+ /*
335
+ * Every incident and episode carries one public note with one attachment,
336
+ * and every incident one postmortem attachment. Their ids are the owner's id
337
+ * with one digit changed, so each is distinct and easy to trace back.
338
+ */
339
+ function noteIdFor(id: string): string {
340
+ return `${id.slice(0, 9)}9${id.slice(10)}`;
341
+ }
342
+
343
+ function fileIdFor(id: string): string {
344
+ return `${id.slice(0, 10)}8${id.slice(11)}`;
345
+ }
346
+
347
+ function titleOf(incidentId: string): string {
348
+ return INCIDENTS.find((incident: IncidentFixture) => {
349
+ return incident.id === incidentId;
350
+ })!.title;
351
+ }
352
+
353
+ function pageName(pageId: string): string {
354
+ return (
355
+ PAGES.find((page: PageFixture) => {
356
+ return page.id === pageId;
357
+ })?.name || "A deleted site"
358
+ );
359
+ }
360
+
361
+ function episodeTitleOf(episodeId: string): string {
362
+ return EPISODES.find((episode: EpisodeFixture) => {
363
+ return episode.id === episodeId;
364
+ })!.title;
365
+ }
366
+
367
+ // Test-table rows with a readable label first, for the tests' names.
368
+ function labelled<T extends Array<unknown>>(
369
+ rows: Array<T>,
370
+ label: (...row: T) => string,
371
+ ): Array<[string, ...T]> {
372
+ return rows.map((row: T): [string, ...T] => {
373
+ return [label(...row), ...row];
374
+ });
375
+ }
376
+
377
+ function pageCases(pageIds: Array<string>): Array<[string, string]> {
378
+ return labelled(
379
+ pageIds.map((pageId: string): [string] => {
380
+ return [pageId];
381
+ }),
382
+ pageName,
383
+ );
384
+ }
385
+
386
+ function shownLabel(shown: boolean): string {
387
+ return shown ? "shown" : "not shown";
388
+ }
389
+
390
+ /*
391
+ * The in-memory database's answers to a query: the few operators the status
392
+ * page code uses (QueryHelper.any / inBetween build Raw operators whose
393
+ * parameters are what they compare with).
394
+ */
395
+ function operatorParameters(operator: unknown): Array<unknown> | null {
396
+ const raw: { objectLiteralParameters?: Dictionary<unknown> } = operator as {
397
+ objectLiteralParameters?: Dictionary<unknown>;
398
+ };
399
+
400
+ if (!raw || typeof raw !== "object" || !("objectLiteralParameters" in raw)) {
401
+ return null;
402
+ }
403
+
404
+ return Object.values(raw.objectLiteralParameters || {});
405
+ }
406
+
407
+ function matchesValue(actual: unknown, expected: unknown): boolean {
408
+ const parameters: Array<unknown> | null = operatorParameters(expected);
409
+
410
+ if (parameters) {
411
+ // QueryHelper.any([]) is TRUE = FALSE.
412
+ if (parameters.length === 0) {
413
+ return false;
414
+ }
415
+
416
+ // QueryHelper.any: IN (...).
417
+ if (parameters.length === 1 && Array.isArray(parameters[0])) {
418
+ return (parameters[0] as Array<unknown>)
419
+ .map((value: unknown): string => {
420
+ return String(value).toLowerCase();
421
+ })
422
+ .includes(String(actual).toLowerCase());
423
+ }
424
+
425
+ // QueryHelper.inBetween: two dates.
426
+ if (
427
+ parameters.length === 2 &&
428
+ parameters[0] instanceof Date &&
429
+ parameters[1] instanceof Date
430
+ ) {
431
+ const time: number = (actual as Date).getTime();
432
+ return parameters[0].getTime() <= time && time <= parameters[1].getTime();
433
+ }
434
+
435
+ throw new Error(`Unexpected operator in a test query: ${String(expected)}`);
436
+ }
437
+
438
+ if (typeof expected === "boolean") {
439
+ return actual === expected;
440
+ }
441
+
442
+ return String(actual).toLowerCase() === String(expected).toLowerCase();
443
+ }
444
+
445
+ function toIdList(value: unknown): Array<string> {
446
+ return (value as Array<ObjectID | string | { _id?: string }>).map(
447
+ (item: ObjectID | string | { _id?: string }): string => {
448
+ if (item instanceof ObjectID || typeof item === "string") {
449
+ return item.toString().toLowerCase();
450
+ }
451
+
452
+ return String(item._id).toLowerCase();
453
+ },
454
+ );
455
+ }
456
+
457
+ function incidentMatches(
458
+ incident: IncidentFixture,
459
+ query: Dictionary<unknown>,
460
+ ): boolean {
461
+ return Object.entries(query).every(
462
+ ([key, expected]: [string, unknown]): boolean => {
463
+ switch (key) {
464
+ case "_id":
465
+ return matchesValue(incident.id, expected);
466
+ case "projectId":
467
+ return matchesValue(incident.projectId.toString(), expected);
468
+ case "isVisibleOnStatusPage":
469
+ return matchesValue(incident.isVisibleOnStatusPage, expected);
470
+ case "showPostmortemOnStatusPage":
471
+ return matchesValue(true, expected);
472
+ case "isScopedToStatusPages":
473
+ return matchesValue(incident.scopedTo !== null, expected);
474
+ case "currentIncidentStateId":
475
+ return matchesValue(UNRESOLVED_STATE_ID, expected);
476
+ case "createdAt":
477
+ case "declaredAt":
478
+ return matchesValue(incident.createdAt, expected);
479
+ case "monitors": {
480
+ // The IncidentMonitor join.
481
+ const monitorIds: Array<string> = toIdList(expected);
482
+ return incident.monitorIds.some((id: string): boolean => {
483
+ return monitorIds.includes(id);
484
+ });
485
+ }
486
+ case "statusPages": {
487
+ // The IncidentStatusPage join.
488
+ const statusPageIds: Array<string> = toIdList(expected);
489
+ return (incident.scopedTo || []).some((id: string): boolean => {
490
+ return statusPageIds.includes(id);
491
+ });
492
+ }
493
+ default:
494
+ throw new Error(`Unexpected incident query key in a test: ${key}`);
495
+ }
496
+ },
497
+ );
498
+ }
499
+
500
+ /*
501
+ * The incident as a row of the database, with every column; a read returns
502
+ * only what it selects of it.
503
+ */
504
+ function incidentRow(fixture: IncidentFixture): Dictionary<unknown> {
505
+ const postmortemAttachment: File = new File();
506
+ postmortemAttachment._id = fileIdFor(fixture.id);
507
+ postmortemAttachment.name = `postmortem-${fixture.id}.pdf`;
508
+ postmortemAttachment.file = Buffer.from("postmortem");
509
+
510
+ const monitors: Array<Monitor> = fixture.monitorIds.map(
511
+ (monitorId: string): Monitor => {
512
+ const monitor: Monitor = new Monitor();
513
+ monitor._id = monitorId;
514
+ return monitor;
515
+ },
516
+ );
517
+
518
+ const statusPages: Array<StatusPage> = (fixture.scopedTo || []).map(
519
+ (statusPageId: string): StatusPage => {
520
+ const statusPage: StatusPage = new StatusPage();
521
+ statusPage._id = statusPageId;
522
+ statusPage.name = pageName(statusPageId);
523
+ return statusPage;
524
+ },
525
+ );
526
+
527
+ return {
528
+ _id: fixture.id,
529
+ projectId: fixture.projectId,
530
+ title: fixture.title,
531
+ description: `${fixture.title}, in detail`,
532
+ createdAt: fixture.createdAt,
533
+ declaredAt: fixture.createdAt,
534
+ updatedAt: fixture.createdAt,
535
+ isVisibleOnStatusPage: fixture.isVisibleOnStatusPage,
536
+ showPostmortemOnStatusPage: true,
537
+ postmortemNote: `${fixture.title}: what happened`,
538
+ postmortemPostedAt: fixture.createdAt,
539
+ postmortemAttachments: [postmortemAttachment],
540
+ currentIncidentStateId: new ObjectID(UNRESOLVED_STATE_ID),
541
+ currentIncidentState: makeState(),
542
+ monitors: monitors,
543
+ isScopedToStatusPages: fixture.scopedTo !== null,
544
+ statusPages: statusPages,
545
+ // The pages told about it when it was created: its scope.
546
+ statusPagesNotifiedOnCreation: fixture.scopedTo || [],
547
+ };
548
+ }
549
+
550
+ /*
551
+ * When set, every incident read also returns the scope columns, whatever it
552
+ * selected: the case the serializers' second line of defence is for.
553
+ */
554
+ let leakScopeColumns: boolean = false;
555
+
556
+ function readIncident(
557
+ fixture: IncidentFixture,
558
+ select: Dictionary<unknown>,
559
+ ): Incident {
560
+ const row: Dictionary<unknown> = incidentRow(fixture);
561
+ const incident: Incident = new Incident();
562
+ const target: Dictionary<unknown> =
563
+ incident as unknown as Dictionary<unknown>;
564
+
565
+ for (const key of Object.keys(select)) {
566
+ if (select[key] && key in row) {
567
+ target[key] = row[key];
568
+ }
569
+ }
570
+
571
+ target["_id"] = row["_id"];
572
+
573
+ if (leakScopeColumns) {
574
+ for (const column of INCIDENT_SCOPE_COLUMNS) {
575
+ target[column] = row[column];
576
+ }
577
+ }
578
+
579
+ return incident;
580
+ }
581
+
582
+ function sortIncidents(
583
+ incidents: Array<IncidentFixture>,
584
+ sort: Dictionary<SortOrder> | undefined,
585
+ ): Array<IncidentFixture> {
586
+ const entries: Array<[string, SortOrder]> = Object.entries(sort || {});
587
+
588
+ return [...incidents].sort(
589
+ (a: IncidentFixture, b: IncidentFixture): number => {
590
+ for (const [key, order] of entries) {
591
+ if (key !== "createdAt" && key !== "declaredAt") {
592
+ throw new Error(`Unexpected incident sort key in a test: ${key}`);
593
+ }
594
+
595
+ const compared: number = a.createdAt.getTime() - b.createdAt.getTime();
596
+
597
+ if (compared !== 0) {
598
+ return order === SortOrder.Descending ? -compared : compared;
599
+ }
600
+ }
601
+
602
+ return 0;
603
+ },
604
+ );
605
+ }
606
+
607
+ function makeState(): IncidentState {
608
+ const state: IncidentState = new IncidentState();
609
+ state._id = UNRESOLVED_STATE_ID;
610
+ state.name = "Investigating";
611
+ return state;
612
+ }
613
+
614
+ function pageFixture(pageId: string): PageFixture | undefined {
615
+ return PAGES.find((page: PageFixture) => {
616
+ return page.id === pageId.toString().toLowerCase();
617
+ });
618
+ }
619
+
620
+ // A status page row, with only what the read selects.
621
+ function readPage(
622
+ pageId: string,
623
+ select: Dictionary<unknown> | undefined,
624
+ ): StatusPage | null {
625
+ const fixture: PageFixture | undefined = pageFixture(pageId);
626
+
627
+ if (!fixture) {
628
+ return null;
629
+ }
630
+
631
+ const row: Dictionary<unknown> = {
632
+ _id: fixture.id,
633
+ projectId: PROJECT_ID,
634
+ name: fixture.name,
635
+ isPublicStatusPage: true,
636
+ showIncidentsOnStatusPage: true,
637
+ showEpisodesOnStatusPage: true,
638
+ showIncidentHistoryInDays: 14,
639
+ showEpisodeHistoryInDays: 14,
640
+ showUptimeHistoryInDays: 14,
641
+ onlyShowScopedIncidents: fixture.onlyShowScopedIncidents,
642
+ };
643
+
644
+ const page: StatusPage = new StatusPage();
645
+ const target: Dictionary<unknown> = page as unknown as Dictionary<unknown>;
646
+
647
+ for (const key of Object.keys(select || {})) {
648
+ if ((select || {})[key] && key in row) {
649
+ target[key] = row[key];
650
+ }
651
+ }
652
+
653
+ target["_id"] = row["_id"];
654
+
655
+ return page;
656
+ }
657
+
658
+ function resourcesOn(pageId: string): Array<StatusPageResource> {
659
+ return (pageFixture(pageId)?.monitorIds || []).map(
660
+ (monitorId: string, index: number): StatusPageResource => {
661
+ const resource: StatusPageResource = new StatusPageResource();
662
+ resource._id = `${pageId.slice(0, -4)}${(1000 + index).toString()}`;
663
+ resource.statusPageId = new ObjectID(pageId);
664
+ resource.monitorId = new ObjectID(monitorId);
665
+ resource.displayName = `Checkout ${index}`;
666
+ return resource;
667
+ },
668
+ );
669
+ }
670
+
671
+ function members(): Array<IncidentEpisodeMember> {
672
+ return EPISODES.flatMap((episode: EpisodeFixture) => {
673
+ return episode.memberIncidentIds.map(
674
+ (incidentId: string): IncidentEpisodeMember => {
675
+ const member: IncidentEpisodeMember = new IncidentEpisodeMember();
676
+ member._id = `${episode.id.slice(0, -4)}${incidentId.slice(-4)}`;
677
+ member.incidentEpisodeId = new ObjectID(episode.id);
678
+ member.incidentId = new ObjectID(incidentId);
679
+ return member;
680
+ },
681
+ );
682
+ });
683
+ }
684
+
685
+ function makeEpisode(fixture: EpisodeFixture): IncidentEpisode {
686
+ const episode: IncidentEpisode = new IncidentEpisode();
687
+ episode._id = fixture.id;
688
+ episode.title = fixture.title;
689
+ episode.createdAt = hoursAgo(1);
690
+ episode.declaredAt = hoursAgo(1);
691
+ episode.currentIncidentState = makeState();
692
+ return episode;
693
+ }
694
+
695
+ function episodeMatches(
696
+ fixture: EpisodeFixture,
697
+ query: Dictionary<unknown>,
698
+ ): boolean {
699
+ return Object.entries(query).every(
700
+ ([key, expected]: [string, unknown]): boolean => {
701
+ switch (key) {
702
+ case "_id":
703
+ return matchesValue(fixture.id, expected);
704
+ case "projectId":
705
+ return matchesValue(PROJECT_ID.toString(), expected);
706
+ case "isVisibleOnStatusPage":
707
+ return matchesValue(true, expected);
708
+ case "currentIncidentStateId":
709
+ return matchesValue(UNRESOLVED_STATE_ID, expected);
710
+ default:
711
+ throw new Error(`Unexpected episode query key in a test: ${key}`);
712
+ }
713
+ },
714
+ );
715
+ }
716
+
717
+ type IncidentRead = {
718
+ query: Dictionary<unknown>;
719
+ select: Dictionary<unknown>;
720
+ limit: number;
721
+ };
722
+
723
+ let incidentReads: Array<IncidentRead> = [];
724
+
725
+ function mockDatabase(): void {
726
+ // The per-request read access check: every page here is public.
727
+ (
728
+ jest.spyOn(StatusPageService, "findOneById") as unknown as jest.SpyInstance
729
+ ).mockImplementation((args: unknown) => {
730
+ const { id } = args as { id: ObjectID };
731
+ const page: StatusPage | null = readPage(id.toString(), {
732
+ isPublicStatusPage: true,
733
+ });
734
+ return Promise.resolve(page);
735
+ });
736
+
737
+ (
738
+ jest.spyOn(StatusPageService, "findOneBy") as unknown as jest.SpyInstance
739
+ ).mockImplementation((args: unknown) => {
740
+ const { query, select } = args as {
741
+ query: Dictionary<unknown>;
742
+ select: Dictionary<unknown>;
743
+ };
744
+ return Promise.resolve(readPage(String(query["_id"]), select));
745
+ });
746
+
747
+ (
748
+ jest.spyOn(
749
+ StatusPageService,
750
+ "getStatusPageResources",
751
+ ) as unknown as jest.SpyInstance
752
+ ).mockImplementation((args: unknown) => {
753
+ const { statusPageId } = args as { statusPageId: ObjectID };
754
+ return Promise.resolve(resourcesOn(statusPageId.toString()));
755
+ });
756
+
757
+ (
758
+ jest.spyOn(
759
+ StatusPageService,
760
+ "getMonitorIdsOnStatusPage",
761
+ ) as unknown as jest.SpyInstance
762
+ ).mockImplementation((args: unknown) => {
763
+ const { statusPageId } = args as { statusPageId: ObjectID };
764
+ return Promise.resolve({
765
+ monitorsOnStatusPage: (
766
+ pageFixture(statusPageId.toString())?.monitorIds || []
767
+ ).map((id: string): ObjectID => {
768
+ return new ObjectID(id);
769
+ }),
770
+ monitorsInGroup: {},
771
+ });
772
+ });
773
+
774
+ (
775
+ jest.spyOn(
776
+ StatusPageResourceService,
777
+ "findBy",
778
+ ) as unknown as jest.SpyInstance
779
+ ).mockImplementation((args: unknown) => {
780
+ const { query } = args as { query: Dictionary<unknown> };
781
+ return Promise.resolve(resourcesOn(String(query["statusPageId"])));
782
+ });
783
+
784
+ for (const [service, method, value] of [
785
+ [MonitorStatusService, "findBy", []],
786
+ [StatusPageGroupService, "findBy", []],
787
+ [MonitorGroupService, "getMonitorGroupResourcesByGroupIds", {}],
788
+ [MonitorGroupService, "getCurrentStatusesForMonitorGroups", {}],
789
+ [StatusPageService, "getMonitorStatusTimelineForStatusPage", []],
790
+ [StatusPageHistoryChartBarColorRuleService, "findBy", []],
791
+ [IncidentStateService, "findBy", []],
792
+ [IncidentStateTimelineService, "findBy", []],
793
+ [IncidentEpisodePublicNoteService, "findBy", []],
794
+ [IncidentEpisodeStateTimelineService, "findBy", []],
795
+ ] as Array<[unknown, string, unknown]>) {
796
+ (
797
+ jest.spyOn(
798
+ service as never,
799
+ method as never,
800
+ ) as unknown as jest.SpyInstance
801
+ ).mockResolvedValue(value as never);
802
+ }
803
+
804
+ (
805
+ jest.spyOn(
806
+ IncidentStateService,
807
+ "getUnresolvedIncidentStates",
808
+ ) as unknown as jest.SpyInstance
809
+ ).mockResolvedValue([makeState()] as never);
810
+
811
+ (
812
+ jest.spyOn(IncidentService, "findBy") as unknown as jest.SpyInstance
813
+ ).mockImplementation((args: unknown) => {
814
+ const { query, select, sort, limit, skip } = args as {
815
+ query: Dictionary<unknown>;
816
+ select: Dictionary<unknown>;
817
+ sort?: Dictionary<SortOrder>;
818
+ limit: number;
819
+ skip: number;
820
+ };
821
+
822
+ incidentReads.push({ query, select, limit });
823
+
824
+ const rows: Array<IncidentFixture> = sortIncidents(
825
+ INCIDENTS.filter((incident: IncidentFixture) => {
826
+ return incidentMatches(incident, query);
827
+ }),
828
+ sort,
829
+ ).slice(skip || 0, (skip || 0) + limit);
830
+
831
+ return Promise.resolve(
832
+ rows.map((row: IncidentFixture): Incident => {
833
+ return readIncident(row, select);
834
+ }),
835
+ );
836
+ });
837
+
838
+ // Nothing on a status page may read incidents without the scope helper.
839
+ for (const method of ["findOneBy", "findAllBy", "countBy"]) {
840
+ (
841
+ jest.spyOn(
842
+ IncidentService,
843
+ method as never,
844
+ ) as unknown as jest.SpyInstance
845
+ ).mockImplementation(() => {
846
+ throw new Error(`IncidentService.${method} is not expected here`);
847
+ });
848
+ }
849
+
850
+ (
851
+ jest.spyOn(
852
+ IncidentPublicNoteService,
853
+ "findBy",
854
+ ) as unknown as jest.SpyInstance
855
+ ).mockImplementation((args: unknown) => {
856
+ const { query } = args as { query: Dictionary<unknown> };
857
+
858
+ return Promise.resolve(
859
+ INCIDENTS.filter((incident: IncidentFixture) => {
860
+ return matchesValue(incident.id, query["incidentId"]);
861
+ }).map((incident: IncidentFixture): IncidentPublicNote => {
862
+ const note: IncidentPublicNote = new IncidentPublicNote();
863
+ note._id = noteIdFor(incident.id);
864
+ note.incidentId = new ObjectID(incident.id);
865
+ note.note = `Update on ${incident.title}`;
866
+ note.postedAt = incident.createdAt;
867
+ return note;
868
+ }),
869
+ );
870
+ });
871
+
872
+ (
873
+ jest.spyOn(
874
+ IncidentPublicNoteService,
875
+ "findOneBy",
876
+ ) as unknown as jest.SpyInstance
877
+ ).mockImplementation((args: unknown) => {
878
+ const { query } = args as { query: Dictionary<unknown> };
879
+ const incidentId: string = String(query["incidentId"]).toLowerCase();
880
+
881
+ if (String(query["_id"]).toLowerCase() !== noteIdFor(incidentId)) {
882
+ return Promise.resolve(null);
883
+ }
884
+
885
+ const attachment: File = new File();
886
+ attachment._id = fileIdFor(noteIdFor(incidentId));
887
+ attachment.name = "note.pdf";
888
+ attachment.file = Buffer.from("note");
889
+
890
+ const note: IncidentPublicNote = new IncidentPublicNote();
891
+ note._id = noteIdFor(incidentId);
892
+ note.attachments = [attachment];
893
+ return Promise.resolve(note);
894
+ });
895
+
896
+ (
897
+ jest.spyOn(
898
+ IncidentEpisodeMemberService,
899
+ "findBy",
900
+ ) as unknown as jest.SpyInstance
901
+ ).mockImplementation((args: unknown) => {
902
+ const { query } = args as { query: Dictionary<unknown> };
903
+
904
+ return Promise.resolve(
905
+ members().filter((member: IncidentEpisodeMember): boolean => {
906
+ if (query["incidentId"] !== undefined) {
907
+ return matchesValue(
908
+ member.incidentId!.toString(),
909
+ query["incidentId"],
910
+ );
911
+ }
912
+
913
+ return matchesValue(
914
+ member.incidentEpisodeId!.toString(),
915
+ query["incidentEpisodeId"],
916
+ );
917
+ }),
918
+ );
919
+ });
920
+
921
+ (
922
+ jest.spyOn(IncidentEpisodeService, "countBy") as unknown as jest.SpyInstance
923
+ ).mockImplementation((args: unknown) => {
924
+ const { query } = args as { query: Dictionary<unknown> };
925
+ return Promise.resolve(
926
+ new PositiveNumber(
927
+ EPISODES.filter((episode: EpisodeFixture) => {
928
+ return episodeMatches(episode, query);
929
+ }).length,
930
+ ),
931
+ );
932
+ });
933
+
934
+ (
935
+ jest.spyOn(IncidentEpisodeService, "findBy") as unknown as jest.SpyInstance
936
+ ).mockImplementation((args: unknown) => {
937
+ const { query } = args as { query: Dictionary<unknown> };
938
+ return Promise.resolve(
939
+ EPISODES.filter((episode: EpisodeFixture) => {
940
+ return episodeMatches(episode, query);
941
+ }).map(makeEpisode),
942
+ );
943
+ });
944
+
945
+ (
946
+ jest.spyOn(
947
+ IncidentEpisodeService,
948
+ "findOneBy",
949
+ ) as unknown as jest.SpyInstance
950
+ ).mockImplementation((args: unknown) => {
951
+ const { query } = args as { query: Dictionary<unknown> };
952
+ const episode: EpisodeFixture | undefined = EPISODES.find(
953
+ (fixture: EpisodeFixture) => {
954
+ return episodeMatches(fixture, query);
955
+ },
956
+ );
957
+ return Promise.resolve(episode ? makeEpisode(episode) : null);
958
+ });
959
+
960
+ (
961
+ jest.spyOn(
962
+ IncidentEpisodePublicNoteService,
963
+ "findOneBy",
964
+ ) as unknown as jest.SpyInstance
965
+ ).mockImplementation((args: unknown) => {
966
+ const { query } = args as { query: Dictionary<unknown> };
967
+ const episodeId: string = String(query["incidentEpisodeId"]).toLowerCase();
968
+
969
+ if (String(query["_id"]).toLowerCase() !== noteIdFor(episodeId)) {
970
+ return Promise.resolve(null);
971
+ }
972
+
973
+ const attachment: File = new File();
974
+ attachment._id = fileIdFor(noteIdFor(episodeId));
975
+ attachment.name = "episode-note.pdf";
976
+ attachment.file = Buffer.from("episode note");
977
+
978
+ const note: IncidentEpisodePublicNote = new IncidentEpisodePublicNote();
979
+ note._id = noteIdFor(episodeId);
980
+ note.attachments = [attachment];
981
+ return Promise.resolve(note);
982
+ });
983
+ }
984
+
985
+ type RouteResult = {
986
+ payload: JSONObject | null;
987
+ error: unknown;
988
+ };
989
+
990
+ async function invokeRoute(data: {
991
+ method: "get" | "post";
992
+ route: string;
993
+ params: Dictionary<string>;
994
+ }): Promise<RouteResult> {
995
+ const req: ExpressRequest = {
996
+ params: data.params,
997
+ body: {},
998
+ query: {},
999
+ cookies: {},
1000
+ headers: {},
1001
+ socket: {},
1002
+ ips: [],
1003
+ } as unknown as ExpressRequest;
1004
+ const res: ExpressResponse = {
1005
+ send: jest.fn(),
1006
+ json: jest.fn(),
1007
+ setHeader: jest.fn(),
1008
+ status: jest.fn().mockReturnThis(),
1009
+ } as unknown as ExpressResponse;
1010
+ const next: jest.Mock = jest.fn() as unknown as jest.Mock;
1011
+ const responseMock: jest.Mock =
1012
+ Response.sendJsonObjectResponse as unknown as jest.Mock;
1013
+ const previousResponseCount: number = responseMock.mock.calls.length;
1014
+
1015
+ await mockRouter
1016
+ .match(data.method, data.route)
1017
+ .handlerFunction(req, res, next as unknown as NextFunction);
1018
+ // Let the overview's detached cache-population chain settle.
1019
+ await new Promise<void>((resolve: () => void) => {
1020
+ setTimeout(resolve, 0);
1021
+ });
1022
+
1023
+ if (next.mock.calls.length > 0) {
1024
+ return { payload: null, error: next.mock.calls[0]![0] };
1025
+ }
1026
+
1027
+ const sent: Array<unknown> | undefined =
1028
+ responseMock.mock.calls[previousResponseCount];
1029
+
1030
+ return {
1031
+ payload: sent ? (sent[2] as JSONObject) : null,
1032
+ error: undefined,
1033
+ };
1034
+ }
1035
+
1036
+ async function getJson(data: {
1037
+ method?: "get" | "post";
1038
+ route: string;
1039
+ params: Dictionary<string>;
1040
+ }): Promise<JSONObject> {
1041
+ const result: RouteResult = await invokeRoute({
1042
+ method: data.method || "post",
1043
+ route: data.route,
1044
+ params: data.params,
1045
+ });
1046
+
1047
+ expect(result.error).toBeUndefined();
1048
+ expect(result.payload).not.toBeNull();
1049
+ return result.payload!;
1050
+ }
1051
+
1052
+ function idsOf(items: unknown): Array<string> {
1053
+ return ((items as JSONArray) || [])
1054
+ .map((item: unknown): string => {
1055
+ return String((item as JSONObject)["_id"]).toLowerCase();
1056
+ })
1057
+ .sort();
1058
+ }
1059
+
1060
+ function sorted(ids: Array<string>): Array<string> {
1061
+ return [...ids].sort();
1062
+ }
1063
+
1064
+ // Every key anywhere in a JSON value.
1065
+ function allKeys(value: unknown, keys: Set<string> = new Set()): Set<string> {
1066
+ if (Array.isArray(value)) {
1067
+ for (const item of value) {
1068
+ allKeys(item, keys);
1069
+ }
1070
+ } else if (value && typeof value === "object") {
1071
+ for (const [key, child] of Object.entries(
1072
+ value as Record<string, unknown>,
1073
+ )) {
1074
+ keys.add(key);
1075
+ allKeys(child, keys);
1076
+ }
1077
+ }
1078
+
1079
+ return keys;
1080
+ }
1081
+
1082
+ // The monitors an episode is shown against, as the page's JSON has them.
1083
+ function episodeMonitors(
1084
+ payload: JSONObject,
1085
+ key: string,
1086
+ id: string,
1087
+ ): Array<string> {
1088
+ const episode: JSONObject | undefined = (payload[key] as JSONArray).find(
1089
+ (item: unknown) => {
1090
+ return (item as JSONObject)["_id"] === id;
1091
+ },
1092
+ ) as JSONObject | undefined;
1093
+
1094
+ return idsOf(episode?.["monitors"]);
1095
+ }
1096
+
1097
+ describe("StatusPageAPI shows an incident only on the status pages in its scope", () => {
1098
+ let api: StatusPageAPI;
1099
+
1100
+ beforeAll(() => {
1101
+ mockRouter.routes.length = 0;
1102
+ api = new StatusPageAPI();
1103
+ });
1104
+
1105
+ beforeEach(() => {
1106
+ jest.clearAllMocks();
1107
+ StatusPageAPI.clearOverviewResponseCache();
1108
+ leakScopeColumns = false;
1109
+ incidentReads = [];
1110
+ mockDatabase();
1111
+ });
1112
+
1113
+ afterEach(() => {
1114
+ jest.restoreAllMocks();
1115
+ StatusPageAPI.clearOverviewResponseCache();
1116
+ });
1117
+
1118
+ describe("the overview", () => {
1119
+ it.each(pageCases(ALL_SITES))(
1120
+ "shows %s only the active incidents in its scope",
1121
+ async (_page: string, pageId: string) => {
1122
+ const payload: JSONObject = await getJson({
1123
+ route: OVERVIEW_ROUTE,
1124
+ params: { statusPageIdOrDomain: pageId },
1125
+ });
1126
+
1127
+ expect(idsOf(payload["activeIncidents"])).toEqual(
1128
+ sorted(INCIDENTS_SHOWN[pageId]!),
1129
+ );
1130
+ },
1131
+ );
1132
+
1133
+ it.each(pageCases(ALL_SITES))(
1134
+ "names on %s's uptime bars only the incidents in its scope",
1135
+ async (_page: string, pageId: string) => {
1136
+ const payload: JSONObject = await getJson({
1137
+ route: OVERVIEW_ROUTE,
1138
+ params: { statusPageIdOrDomain: pageId },
1139
+ });
1140
+
1141
+ expect(idsOf(payload["timelineIncidents"])).toEqual(
1142
+ sorted(INCIDENTS_SHOWN[pageId]!),
1143
+ );
1144
+ },
1145
+ );
1146
+
1147
+ it.each(pageCases([SITE_A, SITE_B, SITE_C]))(
1148
+ "sends %s the public notes of only the incidents it shows",
1149
+ async (_page: string, pageId: string) => {
1150
+ const payload: JSONObject = await getJson({
1151
+ route: OVERVIEW_ROUTE,
1152
+ params: { statusPageIdOrDomain: pageId },
1153
+ });
1154
+
1155
+ const notes: JSONArray = payload["incidentPublicNotes"] as JSONArray;
1156
+ expect(
1157
+ notes
1158
+ .map((note: unknown): string => {
1159
+ return String(
1160
+ ((note as JSONObject)["incidentId"] as JSONObject)?.["value"] ||
1161
+ (note as JSONObject)["incidentId"],
1162
+ ).toLowerCase();
1163
+ })
1164
+ .sort(),
1165
+ ).toEqual(sorted(INCIDENTS_SHOWN[pageId]!));
1166
+
1167
+ const text: string = JSON.stringify(payload);
1168
+ for (const incidentId of ALL_INCIDENTS) {
1169
+ if (!INCIDENTS_SHOWN[pageId]!.includes(incidentId)) {
1170
+ expect(text).not.toContain(titleOf(incidentId));
1171
+ }
1172
+ }
1173
+ },
1174
+ );
1175
+
1176
+ it.each(pageCases(ALL_SITES))(
1177
+ "shows %s only the active episodes of incidents in its scope",
1178
+ async (_page: string, pageId: string) => {
1179
+ const payload: JSONObject = await getJson({
1180
+ route: OVERVIEW_ROUTE,
1181
+ params: { statusPageIdOrDomain: pageId },
1182
+ });
1183
+
1184
+ expect(idsOf(payload["activeEpisodes"])).toEqual(
1185
+ sorted(EPISODES_SHOWN[pageId]!),
1186
+ );
1187
+ },
1188
+ );
1189
+
1190
+ it("shows an episode against the monitors of only its members in the page's scope", async () => {
1191
+ const siteA: JSONObject = await getJson({
1192
+ route: OVERVIEW_ROUTE,
1193
+ params: { statusPageIdOrDomain: SITE_A },
1194
+ });
1195
+ expect(episodeMonitors(siteA, "activeEpisodes", EPISODE_MIXED)).toEqual(
1196
+ sorted([SHARED_MONITOR, SECOND_MONITOR]),
1197
+ );
1198
+
1199
+ /*
1200
+ * Site B lists the second monitor too, but the incident on it is
1201
+ * limited to Site A: the episode is not shown against it on Site B.
1202
+ */
1203
+ const siteB: JSONObject = await getJson({
1204
+ route: OVERVIEW_ROUTE,
1205
+ params: { statusPageIdOrDomain: SITE_B },
1206
+ });
1207
+ expect(episodeMonitors(siteB, "activeEpisodes", EPISODE_MIXED)).toEqual([
1208
+ SHARED_MONITOR,
1209
+ ]);
1210
+ });
1211
+
1212
+ it("does not send the page's onlyShowScopedIncidents, which it reads only to filter", async () => {
1213
+ const payload: JSONObject = await getJson({
1214
+ route: OVERVIEW_ROUTE,
1215
+ params: { statusPageIdOrDomain: SITE_C },
1216
+ });
1217
+
1218
+ const statusPage: JSONObject = payload["statusPage"] as JSONObject;
1219
+ expect(statusPage["_id"]).toBe(SITE_C);
1220
+ expect(statusPage).not.toHaveProperty("onlyShowScopedIncidents");
1221
+ expect(statusPage).not.toHaveProperty("projectId");
1222
+ });
1223
+
1224
+ it("keeps the scoped overview in its cache per page", async () => {
1225
+ const first: JSONObject = await getJson({
1226
+ method: "get",
1227
+ route: OVERVIEW_ROUTE,
1228
+ params: { statusPageIdOrDomain: SITE_B },
1229
+ });
1230
+ const second: JSONObject = await getJson({
1231
+ method: "post",
1232
+ route: OVERVIEW_ROUTE,
1233
+ params: { statusPageIdOrDomain: SITE_A },
1234
+ });
1235
+ const cachedB: JSONObject = await getJson({
1236
+ method: "post",
1237
+ route: OVERVIEW_ROUTE,
1238
+ params: { statusPageIdOrDomain: SITE_B },
1239
+ });
1240
+
1241
+ expect(cachedB).toBe(first);
1242
+ expect(idsOf(cachedB["activeIncidents"])).toEqual([UNSCOPED]);
1243
+ expect(idsOf(second["activeIncidents"])).toEqual(
1244
+ sorted(INCIDENTS_SHOWN[SITE_A]!),
1245
+ );
1246
+ });
1247
+ });
1248
+
1249
+ describe("the incident list and detail", () => {
1250
+ it.each(pageCases(ALL_SITES))(
1251
+ "lists on %s only the incidents in its scope",
1252
+ async (_page: string, pageId: string) => {
1253
+ const payload: JSONObject = await getJson({
1254
+ route: INCIDENTS_ROUTE,
1255
+ params: { statusPageIdOrDomain: pageId },
1256
+ });
1257
+
1258
+ expect(idsOf(payload["incidents"])).toEqual(
1259
+ sorted(INCIDENTS_SHOWN[pageId]!),
1260
+ );
1261
+ },
1262
+ );
1263
+
1264
+ it("lists incidents newest first across the unscoped and scoped halves", async () => {
1265
+ const payload: JSONObject = await getJson({
1266
+ route: INCIDENTS_ROUTE,
1267
+ params: { statusPageIdOrDomain: SITE_A },
1268
+ });
1269
+
1270
+ expect(
1271
+ (payload["incidents"] as JSONArray).map((incident: unknown) => {
1272
+ return (incident as JSONObject)["_id"];
1273
+ }),
1274
+ ).toEqual([SECOND_SCOPED_TO_A, SCOPED_TO_A_AND_C, SCOPED_TO_A, UNSCOPED]);
1275
+ });
1276
+
1277
+ const detailCases: Array<[string, string, boolean]> = ALL_SITES.flatMap(
1278
+ (pageId: string): Array<[string, string, boolean]> => {
1279
+ return ALL_INCIDENTS.map((incidentId: string) => {
1280
+ return [
1281
+ incidentId,
1282
+ pageId,
1283
+ INCIDENTS_SHOWN[pageId]!.includes(incidentId),
1284
+ ];
1285
+ });
1286
+ },
1287
+ );
1288
+
1289
+ it.each(
1290
+ labelled(
1291
+ detailCases,
1292
+ (incidentId: string, pageId: string, shown: boolean): string => {
1293
+ return `"${titleOf(incidentId)}" on ${pageName(pageId)}: ${shownLabel(shown)}`;
1294
+ },
1295
+ ),
1296
+ )(
1297
+ "opens an incident only on the pages in its scope: %s",
1298
+ async (
1299
+ _label: string,
1300
+ incidentId: string,
1301
+ pageId: string,
1302
+ shown: boolean,
1303
+ ) => {
1304
+ const title: string = titleOf(incidentId);
1305
+
1306
+ const payload: JSONObject = await getJson({
1307
+ route: INCIDENT_DETAIL_ROUTE,
1308
+ params: { statusPageIdOrDomain: pageId, incidentId: incidentId },
1309
+ });
1310
+
1311
+ expect(idsOf(payload["incidents"])).toEqual(shown ? [incidentId] : []);
1312
+
1313
+ if (!shown) {
1314
+ // Nothing of it: no note, no title.
1315
+ expect(payload["incidentPublicNotes"]).toEqual([]);
1316
+ expect(JSON.stringify(payload)).not.toContain(title);
1317
+ }
1318
+ },
1319
+ );
1320
+ });
1321
+
1322
+ describe("the episode list and detail", () => {
1323
+ it.each(pageCases(ALL_SITES))(
1324
+ "lists on %s only the episodes of incidents in its scope",
1325
+ async (_page: string, pageId: string) => {
1326
+ const payload: JSONObject = await getJson({
1327
+ route: EPISODES_ROUTE,
1328
+ params: { statusPageIdOrDomain: pageId },
1329
+ });
1330
+
1331
+ expect(idsOf(payload["episodes"])).toEqual(
1332
+ sorted(EPISODES_SHOWN[pageId]!),
1333
+ );
1334
+ },
1335
+ );
1336
+
1337
+ const episodeCases: Array<[string, string, boolean]> = ALL_SITES.flatMap(
1338
+ (pageId: string): Array<[string, string, boolean]> => {
1339
+ return ALL_EPISODES.map((episodeId: string) => {
1340
+ return [
1341
+ episodeId,
1342
+ pageId,
1343
+ EPISODES_SHOWN[pageId]!.includes(episodeId),
1344
+ ];
1345
+ });
1346
+ },
1347
+ );
1348
+
1349
+ it.each(
1350
+ labelled(
1351
+ episodeCases,
1352
+ (episodeId: string, pageId: string, shown: boolean): string => {
1353
+ return `"${episodeTitleOf(episodeId)}" on ${pageName(pageId)}: ${shown ? "shown" : "404"}`;
1354
+ },
1355
+ ),
1356
+ )(
1357
+ "opens an episode by id only on a page one of its incidents is in the scope of: %s",
1358
+ async (
1359
+ _label: string,
1360
+ episodeId: string,
1361
+ pageId: string,
1362
+ shown: boolean,
1363
+ ) => {
1364
+ const result: RouteResult = await invokeRoute({
1365
+ method: "post",
1366
+ route: EPISODE_DETAIL_ROUTE,
1367
+ params: { statusPageIdOrDomain: pageId, episodeId: episodeId },
1368
+ });
1369
+
1370
+ if (shown) {
1371
+ expect(result.error).toBeUndefined();
1372
+ expect(idsOf(result.payload!["episodes"])).toEqual([episodeId]);
1373
+ return;
1374
+ }
1375
+
1376
+ expect(result.error).toBeInstanceOf(NotFoundException);
1377
+ expect((result.error as NotFoundException).message).toBe(
1378
+ "Episode not found",
1379
+ );
1380
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
1381
+ },
1382
+ );
1383
+
1384
+ it("returns 404 for an episode that does not exist", async () => {
1385
+ const result: RouteResult = await invokeRoute({
1386
+ method: "post",
1387
+ route: EPISODE_DETAIL_ROUTE,
1388
+ params: {
1389
+ statusPageIdOrDomain: SITE_A,
1390
+ episodeId: "e0000000-0000-4000-8000-0000000000ee",
1391
+ },
1392
+ });
1393
+
1394
+ expect(result.error).toBeInstanceOf(NotFoundException);
1395
+ });
1396
+
1397
+ it("shows an episode opened by id against the monitors of only its members in the page's scope", async () => {
1398
+ const siteA: JSONObject = await getJson({
1399
+ route: EPISODE_DETAIL_ROUTE,
1400
+ params: { statusPageIdOrDomain: SITE_A, episodeId: EPISODE_MIXED },
1401
+ });
1402
+ expect(episodeMonitors(siteA, "episodes", EPISODE_MIXED)).toEqual(
1403
+ sorted([SHARED_MONITOR, SECOND_MONITOR]),
1404
+ );
1405
+
1406
+ const siteB: JSONObject = await getJson({
1407
+ route: EPISODE_DETAIL_ROUTE,
1408
+ params: { statusPageIdOrDomain: SITE_B, episodeId: EPISODE_MIXED },
1409
+ });
1410
+ expect(episodeMonitors(siteB, "episodes", EPISODE_MIXED)).toEqual([
1411
+ SHARED_MONITOR,
1412
+ ]);
1413
+ });
1414
+
1415
+ it("checks an episode opened by id against the page's monitors and scope, with no history window", async () => {
1416
+ await getJson({
1417
+ route: EPISODE_DETAIL_ROUTE,
1418
+ params: { statusPageIdOrDomain: SITE_B, episodeId: EPISODE_MIXED },
1419
+ });
1420
+
1421
+ const check: Array<IncidentRead> = incidentReads.filter(
1422
+ (read: IncidentRead) => {
1423
+ return (
1424
+ read.limit === 1 &&
1425
+ "monitors" in read.query &&
1426
+ !("createdAt" in read.query)
1427
+ );
1428
+ },
1429
+ );
1430
+
1431
+ // Both halves: unscoped, and limited to Site B.
1432
+ expect(
1433
+ check.map((read: IncidentRead) => {
1434
+ return read.query["isScopedToStatusPages"];
1435
+ }),
1436
+ ).toEqual([false, true]);
1437
+ expect(check[1]!.query["statusPages"]).toEqual([SITE_B]);
1438
+ expect(toIdList(check[0]!.query["monitors"]).sort()).toEqual(
1439
+ sorted([SHARED_MONITOR, SECOND_MONITOR]),
1440
+ );
1441
+ });
1442
+ });
1443
+
1444
+ describe("attachments", () => {
1445
+ async function download(data: {
1446
+ route: string;
1447
+ params: Dictionary<string>;
1448
+ }): Promise<RouteResult> {
1449
+ return await invokeRoute({
1450
+ method: "get",
1451
+ route: data.route,
1452
+ params: data.params,
1453
+ });
1454
+ }
1455
+
1456
+ function expectServed(result: RouteResult, fileId: string): void {
1457
+ expect(result.error).toBeUndefined();
1458
+ const sendFile: jest.Mock =
1459
+ Response.sendFileResponse as unknown as jest.Mock;
1460
+ expect(sendFile).toHaveBeenCalledTimes(1);
1461
+ expect((sendFile.mock.calls[0]![2] as File)._id).toBe(fileId);
1462
+ }
1463
+
1464
+ function expectNotFound(result: RouteResult): void {
1465
+ expect(result.error).toBeInstanceOf(NotFoundException);
1466
+ expect(Response.sendFileResponse).not.toHaveBeenCalled();
1467
+ }
1468
+
1469
+ const postmortemCases: Array<[string, string, boolean]> = [
1470
+ [SCOPED_TO_A, SITE_A, true],
1471
+ [SCOPED_TO_A, SITE_B, false],
1472
+ [SCOPED_TO_A, SITE_C, false],
1473
+ [SCOPED_TO_A_AND_C, SITE_C, true],
1474
+ [UNSCOPED, SITE_B, true],
1475
+ [UNSCOPED, SITE_C, false],
1476
+ [SCOPED_TO_DELETED_PAGE, SITE_A, false],
1477
+ ];
1478
+
1479
+ function incidentAttachmentLabel(
1480
+ incidentId: string,
1481
+ pageId: string,
1482
+ served: boolean,
1483
+ ): string {
1484
+ return `"${titleOf(incidentId)}" on ${pageName(pageId)}: ${served ? "served" : "404"}`;
1485
+ }
1486
+
1487
+ it.each(labelled(postmortemCases, incidentAttachmentLabel))(
1488
+ "serves an incident's postmortem attachment only on the pages in its scope: %s",
1489
+ async (
1490
+ _label: string,
1491
+ incidentId: string,
1492
+ pageId: string,
1493
+ served: boolean,
1494
+ ) => {
1495
+ const result: RouteResult = await download({
1496
+ route: POSTMORTEM_ATTACHMENT_ROUTE,
1497
+ params: {
1498
+ statusPageId: pageId,
1499
+ incidentId: incidentId,
1500
+ fileId: fileIdFor(incidentId),
1501
+ },
1502
+ });
1503
+
1504
+ if (served) {
1505
+ expectServed(result, fileIdFor(incidentId));
1506
+ } else {
1507
+ expectNotFound(result);
1508
+ }
1509
+ },
1510
+ );
1511
+
1512
+ it.each(labelled(postmortemCases, incidentAttachmentLabel))(
1513
+ "serves an incident's public note attachment only on the pages in its scope: %s",
1514
+ async (
1515
+ _label: string,
1516
+ incidentId: string,
1517
+ pageId: string,
1518
+ served: boolean,
1519
+ ) => {
1520
+ const result: RouteResult = await download({
1521
+ route: INCIDENT_NOTE_ATTACHMENT_ROUTE,
1522
+ params: {
1523
+ statusPageId: pageId,
1524
+ incidentId: incidentId,
1525
+ noteId: noteIdFor(incidentId),
1526
+ fileId: fileIdFor(noteIdFor(incidentId)),
1527
+ },
1528
+ });
1529
+
1530
+ if (served) {
1531
+ expectServed(result, fileIdFor(noteIdFor(incidentId)));
1532
+ } else {
1533
+ expectNotFound(result);
1534
+ }
1535
+ },
1536
+ );
1537
+
1538
+ const episodeNoteCases: Array<[string, string, boolean]> = [
1539
+ [EPISODE_SCOPED_TO_A, SITE_A, true],
1540
+ [EPISODE_SCOPED_TO_A, SITE_B, false],
1541
+ [EPISODE_MIXED, SITE_B, true],
1542
+ [EPISODE_MIXED, SITE_C, false],
1543
+ [EPISODE_SCOPED_TO_A_AND_C, SITE_C, true],
1544
+ [EPISODE_NOWHERE, SITE_A, false],
1545
+ ];
1546
+
1547
+ it.each(
1548
+ labelled(
1549
+ episodeNoteCases,
1550
+ (episodeId: string, pageId: string, served: boolean): string => {
1551
+ return `"${episodeTitleOf(episodeId)}" on ${pageName(pageId)}: ${served ? "served" : "404"}`;
1552
+ },
1553
+ ),
1554
+ )(
1555
+ "serves an episode's public note attachment only on a page one of its incidents is in the scope of: %s",
1556
+ async (
1557
+ _label: string,
1558
+ episodeId: string,
1559
+ pageId: string,
1560
+ served: boolean,
1561
+ ) => {
1562
+ const result: RouteResult = await download({
1563
+ route: EPISODE_NOTE_ATTACHMENT_ROUTE,
1564
+ params: {
1565
+ statusPageId: pageId,
1566
+ episodeId: episodeId,
1567
+ noteId: noteIdFor(episodeId),
1568
+ fileId: fileIdFor(noteIdFor(episodeId)),
1569
+ },
1570
+ });
1571
+
1572
+ if (served) {
1573
+ expectServed(result, fileIdFor(noteIdFor(episodeId)));
1574
+ } else {
1575
+ expectNotFound(result);
1576
+ }
1577
+ },
1578
+ );
1579
+ });
1580
+
1581
+ describe("the public JSON never names an incident's scope", () => {
1582
+ type View = {
1583
+ name: string;
1584
+ route: string;
1585
+ params: (pageId: string) => Dictionary<string>;
1586
+ // An id the view shows on Sites A and C.
1587
+ shows: string;
1588
+ };
1589
+
1590
+ const views: Array<View> = [
1591
+ {
1592
+ name: "overview",
1593
+ route: OVERVIEW_ROUTE,
1594
+ params: (pageId: string) => {
1595
+ return { statusPageIdOrDomain: pageId };
1596
+ },
1597
+ shows: SCOPED_TO_A_AND_C,
1598
+ },
1599
+ {
1600
+ name: "incident list",
1601
+ route: INCIDENTS_ROUTE,
1602
+ params: (pageId: string) => {
1603
+ return { statusPageIdOrDomain: pageId };
1604
+ },
1605
+ shows: SCOPED_TO_A_AND_C,
1606
+ },
1607
+ {
1608
+ name: "incident detail",
1609
+ route: INCIDENT_DETAIL_ROUTE,
1610
+ params: (pageId: string) => {
1611
+ return {
1612
+ statusPageIdOrDomain: pageId,
1613
+ incidentId: SCOPED_TO_A_AND_C,
1614
+ };
1615
+ },
1616
+ shows: SCOPED_TO_A_AND_C,
1617
+ },
1618
+ {
1619
+ name: "episode list",
1620
+ route: EPISODES_ROUTE,
1621
+ params: (pageId: string) => {
1622
+ return { statusPageIdOrDomain: pageId };
1623
+ },
1624
+ shows: EPISODE_SCOPED_TO_A_AND_C,
1625
+ },
1626
+ {
1627
+ name: "episode detail",
1628
+ route: EPISODE_DETAIL_ROUTE,
1629
+ params: (pageId: string) => {
1630
+ return {
1631
+ statusPageIdOrDomain: pageId,
1632
+ episodeId: EPISODE_SCOPED_TO_A_AND_C,
1633
+ };
1634
+ },
1635
+ shows: EPISODE_SCOPED_TO_A_AND_C,
1636
+ },
1637
+ ];
1638
+
1639
+ const cases: Array<[string, string, boolean]> = views.flatMap(
1640
+ (view: View): Array<[string, string, boolean]> => {
1641
+ return [SITE_A, SITE_C].flatMap(
1642
+ (pageId: string): Array<[string, string, boolean]> => {
1643
+ return [
1644
+ [view.name, pageId, false],
1645
+ [view.name, pageId, true],
1646
+ ];
1647
+ },
1648
+ );
1649
+ },
1650
+ );
1651
+
1652
+ it.each(
1653
+ labelled(
1654
+ cases,
1655
+ (viewName: string, pageId: string, leak: boolean): string => {
1656
+ return `the ${viewName} of ${pageName(pageId)}${leak ? ", even when every read returns the scope columns" : ""}`;
1657
+ },
1658
+ ),
1659
+ )(
1660
+ "has no scope column: %s",
1661
+ async (
1662
+ _label: string,
1663
+ viewName: string,
1664
+ pageId: string,
1665
+ leak: boolean,
1666
+ ) => {
1667
+ leakScopeColumns = leak;
1668
+
1669
+ const view: View = views.find((item: View) => {
1670
+ return item.name === viewName;
1671
+ })!;
1672
+
1673
+ const payload: JSONObject = await getJson({
1674
+ route: view.route,
1675
+ params: view.params(pageId),
1676
+ });
1677
+
1678
+ const keys: Set<string> = allKeys(payload);
1679
+
1680
+ for (const column of INCIDENT_SCOPE_COLUMNS) {
1681
+ expect(keys.has(column)).toBe(false);
1682
+ }
1683
+
1684
+ // Nor the other pages an incident on this page is limited to.
1685
+ const text: string = JSON.stringify(payload);
1686
+
1687
+ for (const otherPage of [...ALL_SITES, DELETED_SITE]) {
1688
+ if (otherPage !== pageId) {
1689
+ expect(text).not.toContain(otherPage);
1690
+ expect(text).not.toContain(`"${pageName(otherPage)}"`);
1691
+ }
1692
+ }
1693
+
1694
+ // The page still shows what it should.
1695
+ expect(text).toContain(view.shows);
1696
+ },
1697
+ );
1698
+
1699
+ it("no status page read selects an incident's scope columns except the episode members it filters", async () => {
1700
+ for (const pageId of ALL_SITES) {
1701
+ for (const view of views) {
1702
+ await invokeRoute({
1703
+ method: "post",
1704
+ route: view.route,
1705
+ params: view.params(pageId),
1706
+ });
1707
+ }
1708
+ }
1709
+
1710
+ const readsWithScope: Array<IncidentRead> = incidentReads.filter(
1711
+ (read: IncidentRead) => {
1712
+ return INCIDENT_SCOPE_COLUMNS.some((column: string) => {
1713
+ return Boolean(read.select[column]);
1714
+ });
1715
+ },
1716
+ );
1717
+
1718
+ expect(readsWithScope.length).toBeGreaterThan(0);
1719
+
1720
+ for (const read of readsWithScope) {
1721
+ // An episode's members, read by id to map it to the page's monitors.
1722
+ expect(Object.keys(read.query).sort()).toEqual(
1723
+ expect.arrayContaining(["_id", "projectId"]),
1724
+ );
1725
+ expect(read.query).not.toHaveProperty("monitors");
1726
+ expect(read.select).toHaveProperty("monitors");
1727
+ expect(read.select).not.toHaveProperty("title");
1728
+ expect(read.select).not.toHaveProperty("statusPagesNotifiedOnCreation");
1729
+ }
1730
+ });
1731
+ });
1732
+
1733
+ describe("the scope is applied in SQL, before the cap", () => {
1734
+ it("every incident read by a page's monitors is split on isScopedToStatusPages, each half up to LIMIT_PER_PROJECT", async () => {
1735
+ for (const pageId of [SITE_A, SITE_C]) {
1736
+ await getJson({
1737
+ route: OVERVIEW_ROUTE,
1738
+ params: { statusPageIdOrDomain: pageId },
1739
+ });
1740
+ await getJson({
1741
+ route: INCIDENTS_ROUTE,
1742
+ params: { statusPageIdOrDomain: pageId },
1743
+ });
1744
+ await getJson({
1745
+ route: EPISODES_ROUTE,
1746
+ params: { statusPageIdOrDomain: pageId },
1747
+ });
1748
+ }
1749
+
1750
+ const byMonitors: Array<IncidentRead> = incidentReads.filter(
1751
+ (read: IncidentRead) => {
1752
+ return "monitors" in read.query;
1753
+ },
1754
+ );
1755
+
1756
+ expect(byMonitors.length).toBeGreaterThan(0);
1757
+
1758
+ for (const read of byMonitors) {
1759
+ expect(typeof read.query["isScopedToStatusPages"]).toBe("boolean");
1760
+ expect(read.limit).toBe(LIMIT_PER_PROJECT);
1761
+
1762
+ if (read.query["isScopedToStatusPages"] === true) {
1763
+ expect(read.query["statusPages"]).toHaveLength(1);
1764
+ } else {
1765
+ expect(read.query).not.toHaveProperty("statusPages");
1766
+ }
1767
+ }
1768
+
1769
+ // Site C only shows scoped incidents: it never asks for unscoped ones.
1770
+ const unscopedOnSiteC: Array<IncidentRead> = byMonitors.filter(
1771
+ (read: IncidentRead) => {
1772
+ return (
1773
+ read.query["isScopedToStatusPages"] === false &&
1774
+ toIdList(read.query["monitors"]).length === 1
1775
+ );
1776
+ },
1777
+ );
1778
+ expect(unscopedOnSiteC).toEqual([]);
1779
+ });
1780
+ });
1781
+
1782
+ describe("the episode member filter", () => {
1783
+ type MemberFilter = (data: {
1784
+ incidents: Array<Incident>;
1785
+ statusPage: StatusPage;
1786
+ }) => Array<Incident>;
1787
+
1788
+ function filterMembers(
1789
+ pageId: string,
1790
+ select: Dictionary<unknown>,
1791
+ ): Array<Incident> {
1792
+ const filter: MemberFilter = (
1793
+ api as unknown as { keepMemberIncidentsInScope: MemberFilter }
1794
+ ).keepMemberIncidentsInScope.bind(api);
1795
+
1796
+ return filter({
1797
+ incidents: INCIDENTS.map((fixture: IncidentFixture): Incident => {
1798
+ return readIncident(fixture, {
1799
+ _id: true,
1800
+ monitors: true,
1801
+ isScopedToStatusPages: true,
1802
+ statusPages: true,
1803
+ statusPagesNotifiedOnCreation: true,
1804
+ });
1805
+ }),
1806
+ statusPage: readPage(pageId, select)!,
1807
+ });
1808
+ }
1809
+
1810
+ it.each(
1811
+ labelled<[string, Array<string>]>(
1812
+ [
1813
+ [
1814
+ SITE_A,
1815
+ [
1816
+ UNSCOPED,
1817
+ SCOPED_TO_A,
1818
+ SCOPED_TO_A_AND_C,
1819
+ SECOND_SCOPED_TO_A,
1820
+ OTHER_PROJECT_INCIDENT,
1821
+ ],
1822
+ ],
1823
+ // Visibility and project are the member query's to filter, not scope.
1824
+ [SITE_B, [UNSCOPED, HIDDEN_SCOPED_TO_B, OTHER_PROJECT_INCIDENT]],
1825
+ [SITE_C, [SCOPED_TO_A_AND_C]],
1826
+ ] as Array<[string, Array<string>]>,
1827
+ (pageId: string): string => {
1828
+ return pageName(pageId);
1829
+ },
1830
+ ),
1831
+ )(
1832
+ "keeps the members %s shows as far as scope goes",
1833
+ (_page: string, pageId: string, expected: Array<string>) => {
1834
+ expect(
1835
+ filterMembers(pageId, { onlyShowScopedIncidents: true }).map(
1836
+ (incident: Incident): string => {
1837
+ return incident._id!;
1838
+ },
1839
+ ),
1840
+ ).toEqual(expected);
1841
+ },
1842
+ );
1843
+
1844
+ it("keeps no unscoped member for a page whose onlyShowScopedIncidents was not loaded", () => {
1845
+ expect(
1846
+ filterMembers(SITE_B, {}).map((incident: Incident): string => {
1847
+ return incident._id!;
1848
+ }),
1849
+ ).toEqual([HIDDEN_SCOPED_TO_B]);
1850
+ });
1851
+
1852
+ it("removes the scope columns from what it keeps, and leaves the monitors", () => {
1853
+ const kept: Array<Incident> = filterMembers(SITE_A, {
1854
+ onlyShowScopedIncidents: true,
1855
+ });
1856
+
1857
+ expect(kept.length).toBeGreaterThan(0);
1858
+
1859
+ for (const incident of kept) {
1860
+ for (const column of INCIDENT_SCOPE_COLUMNS) {
1861
+ expect(incident).not.toHaveProperty(column);
1862
+ }
1863
+
1864
+ expect(incident.monitors!.length).toBeGreaterThan(0);
1865
+ }
1866
+ });
1867
+ });
1868
+ });