@oneuptime/common 12.0.12 → 12.0.13

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 (234) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  28. package/Server/Utils/LogRedaction.ts +576 -0
  29. package/Server/Utils/Logger.ts +123 -46
  30. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  31. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  32. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  33. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  34. package/Server/Utils/StartServer.ts +30 -9
  35. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  36. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  37. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  38. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  39. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  40. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  41. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  42. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  43. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  44. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  45. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  46. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  47. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  48. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  49. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  50. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  52. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  53. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  54. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  55. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  56. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  57. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  58. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  59. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  60. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  62. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  63. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  65. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  66. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  67. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  68. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  69. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  70. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  71. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  72. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  73. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  74. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  75. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  76. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  77. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  78. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  79. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  80. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  81. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  82. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  83. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  84. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  85. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  86. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  87. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  88. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  89. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  90. package/Types/API/Hostname.ts +79 -0
  91. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  92. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  93. package/Types/Database/DatabaseProperty.ts +38 -0
  94. package/Types/Database/TableColumn.ts +34 -7
  95. package/Types/Events/Recurring.ts +16 -1
  96. package/Types/Monitor/MonitorStep.ts +13 -1
  97. package/Types/Monitor/MonitorType.ts +345 -40
  98. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  99. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  100. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  101. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  102. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  103. package/UI/Components/Footer/Footer.tsx +32 -6
  104. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  105. package/UI/Components/Forms/Types/Field.ts +8 -0
  106. package/UI/Components/Loader/PageLoader.tsx +29 -8
  107. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  108. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  109. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  110. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  111. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  112. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  113. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  114. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  115. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  116. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  118. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  120. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  122. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  124. package/build/dist/Server/API/AIChatAPI.js +48 -35
  125. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  126. package/build/dist/Server/API/CommonAPI.js +59 -0
  127. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  128. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  129. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  130. package/build/dist/Server/API/SlackAPI.js +13 -10
  131. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  132. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  133. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  134. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  135. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  137. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  138. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  142. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  143. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  144. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  145. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  146. package/build/dist/Server/Services/DatabaseService.js +15 -6
  147. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  148. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  149. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  150. package/build/dist/Server/Services/LogService.js +16 -0
  151. package/build/dist/Server/Services/LogService.js.map +1 -1
  152. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  153. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  154. package/build/dist/Server/Services/SpanService.js +16 -0
  155. package/build/dist/Server/Services/SpanService.js.map +1 -1
  156. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  157. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  158. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  159. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  160. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  161. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  162. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  163. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  164. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  165. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  166. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  167. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  168. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  169. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  170. package/build/dist/Server/Utils/Logger.js +98 -45
  171. package/build/dist/Server/Utils/Logger.js.map +1 -1
  172. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  173. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  174. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  175. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  176. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  177. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  178. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  179. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  180. package/build/dist/Server/Utils/StartServer.js +10 -7
  181. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  182. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  183. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  184. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  185. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  186. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  187. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  188. package/build/dist/Types/API/Hostname.js +63 -0
  189. package/build/dist/Types/API/Hostname.js.map +1 -1
  190. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  191. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  192. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  193. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  194. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  195. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  196. package/build/dist/Types/Database/TableColumn.js +24 -6
  197. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  198. package/build/dist/Types/Events/Recurring.js +13 -1
  199. package/build/dist/Types/Events/Recurring.js.map +1 -1
  200. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  201. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  202. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  203. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  204. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  205. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  206. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  207. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  208. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  209. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  210. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  211. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  212. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  213. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  214. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  215. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  216. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  217. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  218. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  219. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  220. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  221. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  222. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  223. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  224. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  225. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  226. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  227. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  228. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  229. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  230. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  231. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  232. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  233. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  234. package/package.json +1 -1
@@ -0,0 +1,702 @@
1
+ import WorkspaceNotificationRuleService from "../../../Server/Services/WorkspaceNotificationRuleService";
2
+ import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
3
+ import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
4
+ import WorkspaceNotificationLogService from "../../../Server/Services/WorkspaceNotificationLogService";
5
+ import WorkspaceUtil from "../../../Server/Utils/Workspace/Workspace";
6
+ import {
7
+ WorkspaceChannel,
8
+ WorkspaceSendMessageResponse,
9
+ } from "../../../Server/Utils/Workspace/WorkspaceBase";
10
+ import NotificationRuleWorkspaceChannel from "../../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
11
+ import WorkspaceNotificationRule from "../../../Models/DatabaseModels/WorkspaceNotificationRule";
12
+ import WorkspaceProjectAuthToken, {
13
+ MicrosoftTeamsMiscData,
14
+ } from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
15
+ import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUserAuthToken";
16
+ import WorkspaceNotificationLog from "../../../Models/DatabaseModels/WorkspaceNotificationLog";
17
+ import BaseNotificationRule from "../../../Types/Workspace/NotificationRules/BaseNotificationRule";
18
+ import NotificationRuleEventType from "../../../Types/Workspace/NotificationRules/EventType";
19
+ import FilterCondition from "../../../Types/Filter/FilterCondition";
20
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
21
+ import WorkspaceMessagePayload from "../../../Types/Workspace/WorkspaceMessagePayload";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
24
+
25
+ /*
26
+ * Tests for the CHANNEL half of WorkspaceNotificationRuleService.testRule — the
27
+ * "Test Rule" button an admin presses to prove a workspace integration works.
28
+ *
29
+ * The bug these pin down: the loop that posted the test card into a channel the
30
+ * test had just created was nested inside `if (shouldPostToExistingChannel)`. A
31
+ * rule configured to CREATE a channel and not post to an existing one therefore
32
+ * created the channel, invited nobody (a no-op on Teams) and returned success
33
+ * having sent nothing at all.
34
+ *
35
+ * That is worse than a missing feature. The customer who reported the Microsoft
36
+ * Teams roster failure used exactly this configuration as a control: "creating a
37
+ * channel works, so the app must be installed correctly." It never posted, so it
38
+ * proved nothing, and the real fault was looked for in the wrong place.
39
+ *
40
+ * The created-channel post was also addressed with `existingTeam` — a field
41
+ * belonging to the OTHER half of the rule, undefined for a create-only rule and
42
+ * potentially a different team when both halves are configured.
43
+ */
44
+
45
+ const TEAM_A: string = "aaaaaaaa-1111-2222-3333-444444444444";
46
+ const TEAM_B: string = "bbbbbbbb-5555-6666-7777-888888888888";
47
+
48
+ interface RuleOverrides extends Partial<BaseNotificationRule> {
49
+ shouldCreateNewChannel?: boolean;
50
+ teamToCreateChannelIn?: string;
51
+ newChannelTemplateName?: string;
52
+ }
53
+
54
+ function makeRule(data: {
55
+ workspaceType?: WorkspaceType | undefined;
56
+ overrides: RuleOverrides;
57
+ name?: string | undefined;
58
+ }): WorkspaceNotificationRule {
59
+ const rule: WorkspaceNotificationRule = new WorkspaceNotificationRule();
60
+ rule.id = ObjectID.generate();
61
+ rule.projectId = ObjectID.generate();
62
+ rule.workspaceType = data.workspaceType || WorkspaceType.MicrosoftTeams;
63
+ rule.eventType = NotificationRuleEventType.Alert;
64
+ rule.name = data.name || "Test public channel Team";
65
+ rule.notificationRule = {
66
+ _type: "NotificationRule",
67
+ filterCondition: FilterCondition.Any,
68
+ filters: [],
69
+ shouldPostToExistingChannel: false,
70
+ existingChannelNames: "",
71
+ shouldCreateNewChannel: false,
72
+ ...data.overrides,
73
+ } as BaseNotificationRule;
74
+ return rule;
75
+ }
76
+
77
+ function makeCreatedChannel(data: {
78
+ id: string;
79
+ name: string;
80
+ teamId?: string | undefined;
81
+ workspaceType?: WorkspaceType | undefined;
82
+ }): NotificationRuleWorkspaceChannel {
83
+ const channel: NotificationRuleWorkspaceChannel = {
84
+ id: data.id,
85
+ name: data.name,
86
+ workspaceType: data.workspaceType || WorkspaceType.MicrosoftTeams,
87
+ notificationRuleId: ObjectID.generate().toString(),
88
+ };
89
+
90
+ if (data.teamId) {
91
+ channel.teamId = data.teamId;
92
+ }
93
+
94
+ return channel;
95
+ }
96
+
97
+ function makeSendResponse(data: {
98
+ workspaceType?: WorkspaceType | undefined;
99
+ threads?: Array<{ id: string; name: string; threadId: string }> | undefined;
100
+ errors?: Array<{ id: string; name: string; error: string }> | undefined;
101
+ }): WorkspaceSendMessageResponse {
102
+ const workspaceType: WorkspaceType =
103
+ data.workspaceType || WorkspaceType.MicrosoftTeams;
104
+
105
+ const response: WorkspaceSendMessageResponse = {
106
+ workspaceType: workspaceType,
107
+ threads: (data.threads || []).map(
108
+ (thread: { id: string; name: string; threadId: string }) => {
109
+ return {
110
+ channel: {
111
+ id: thread.id,
112
+ name: thread.name,
113
+ workspaceType: workspaceType,
114
+ } as WorkspaceChannel,
115
+ threadId: thread.threadId,
116
+ };
117
+ },
118
+ ),
119
+ };
120
+
121
+ if (data.errors) {
122
+ response.errors = data.errors.map(
123
+ (error: { id: string; name: string; error: string }) => {
124
+ return {
125
+ channel: {
126
+ id: error.id,
127
+ name: error.name,
128
+ workspaceType: workspaceType,
129
+ } as WorkspaceChannel,
130
+ error: error.error,
131
+ };
132
+ },
133
+ );
134
+ }
135
+
136
+ return response;
137
+ }
138
+
139
+ interface Mocks {
140
+ postSpy: jest.SpyInstance;
141
+ createChannelsSpy: jest.SpyInstance;
142
+ inviteUsersSpy: jest.SpyInstance;
143
+ doesChannelExistSpy: jest.SpyInstance;
144
+ createLogSpy: jest.SpyInstance;
145
+ }
146
+
147
+ const testByUserId: ObjectID = ObjectID.generate();
148
+
149
+ function mockDeps(data: {
150
+ rule: WorkspaceNotificationRule;
151
+ createdChannels?: Array<NotificationRuleWorkspaceChannel> | undefined;
152
+ responses?: Array<WorkspaceSendMessageResponse> | undefined;
153
+ channelExists?: boolean | undefined;
154
+ }): Mocks {
155
+ jest
156
+ .spyOn(WorkspaceNotificationRuleService, "findOneById")
157
+ .mockResolvedValue(data.rule);
158
+
159
+ const userAuth: WorkspaceUserAuthToken = new WorkspaceUserAuthToken();
160
+ userAuth.workspaceUserId = "teams-user-1";
161
+ jest
162
+ .spyOn(WorkspaceUserAuthTokenService, "findOneBy")
163
+ .mockResolvedValue(userAuth);
164
+
165
+ const projectAuth: WorkspaceProjectAuthToken =
166
+ new WorkspaceProjectAuthToken();
167
+ projectAuth.authToken = "project-auth-token";
168
+ projectAuth.workspaceType = data.rule.workspaceType!;
169
+ jest
170
+ .spyOn(WorkspaceProjectAuthTokenService, "findOneBy")
171
+ .mockResolvedValue(projectAuth);
172
+
173
+ const authWithMisc: WorkspaceProjectAuthToken =
174
+ new WorkspaceProjectAuthToken();
175
+ authWithMisc.miscData = {
176
+ tenantId: "tenant-1",
177
+ teamId: TEAM_A,
178
+ teamName: "Test Team",
179
+ botId: "bot-1",
180
+ } as MicrosoftTeamsMiscData;
181
+ jest
182
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
183
+ .mockResolvedValue(authWithMisc);
184
+
185
+ const createChannelsSpy: jest.SpyInstance = jest
186
+ .spyOn(WorkspaceNotificationRuleService, "createChannelsBasedOnRules")
187
+ .mockResolvedValue(data.createdChannels || []);
188
+
189
+ const inviteUsersSpy: jest.SpyInstance = jest
190
+ .spyOn(
191
+ WorkspaceNotificationRuleService,
192
+ "inviteUsersBasedOnRulesAndWorkspaceChannels",
193
+ )
194
+ .mockResolvedValue(undefined as never);
195
+
196
+ const doesChannelExistSpy: jest.SpyInstance = jest
197
+ .fn()
198
+ .mockResolvedValue(
199
+ data.channelExists === undefined ? true : data.channelExists,
200
+ ) as unknown as jest.SpyInstance;
201
+
202
+ jest.spyOn(WorkspaceUtil, "getWorkspaceTypeUtil").mockReturnValue({
203
+ doesChannelExist: doesChannelExistSpy,
204
+ } as never);
205
+
206
+ const postSpy: jest.SpyInstance = jest
207
+ .spyOn(WorkspaceUtil, "postMessageToAllWorkspaceChannelsAsBot")
208
+ .mockResolvedValue(data.responses || []);
209
+
210
+ const createLogSpy: jest.SpyInstance = jest
211
+ .spyOn(WorkspaceNotificationLogService, "create")
212
+ .mockResolvedValue(new WorkspaceNotificationLog());
213
+
214
+ return {
215
+ postSpy,
216
+ createChannelsSpy,
217
+ inviteUsersSpy,
218
+ doesChannelExistSpy,
219
+ createLogSpy,
220
+ };
221
+ }
222
+
223
+ function runTestRule(rule: WorkspaceNotificationRule): Promise<void> {
224
+ return WorkspaceNotificationRuleService.testRule({
225
+ ruleId: rule.id!,
226
+ projectId: ObjectID.generate(), // testRule overwrites this with rule.projectId.
227
+ testByUserId: testByUserId,
228
+ props: { isRoot: true },
229
+ });
230
+ }
231
+
232
+ /*
233
+ * The single payload handed to postMessageToAllWorkspaceChannelsAsBot on a
234
+ * given call, for asserting destinations and teamId.
235
+ */
236
+ function payloadOfCall(
237
+ postSpy: jest.SpyInstance,
238
+ callIndex: number,
239
+ ): WorkspaceMessagePayload {
240
+ const args: {
241
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
242
+ } = postSpy.mock.calls[callIndex]![0] as {
243
+ messagePayloadsByWorkspace: Array<WorkspaceMessagePayload>;
244
+ };
245
+
246
+ return args.messagePayloadsByWorkspace[0]!;
247
+ }
248
+
249
+ beforeEach(() => {
250
+ jest.restoreAllMocks();
251
+ });
252
+
253
+ afterEach(() => {
254
+ jest.restoreAllMocks();
255
+ });
256
+
257
+ describe("testRule posts to channels it created, even when the rule posts nowhere else", () => {
258
+ test("a create-only rule sends a test message", async () => {
259
+ /*
260
+ * THE regression test. Before the fix this asserted zero calls, silently,
261
+ * and the Dashboard still showed a success modal.
262
+ */
263
+ const rule: WorkspaceNotificationRule = makeRule({
264
+ overrides: {
265
+ shouldCreateNewChannel: true,
266
+ shouldPostToExistingChannel: false,
267
+ teamToCreateChannelIn: TEAM_A,
268
+ newChannelTemplateName: "test public channel",
269
+ },
270
+ });
271
+
272
+ const mocks: Mocks = mockDeps({
273
+ rule: rule,
274
+ createdChannels: [
275
+ makeCreatedChannel({
276
+ id: "19:new-channel@thread.tacv2",
277
+ name: "test-public-channel-ab12c",
278
+ teamId: TEAM_A,
279
+ }),
280
+ ],
281
+ responses: [
282
+ makeSendResponse({
283
+ threads: [
284
+ {
285
+ id: "19:new-channel@thread.tacv2",
286
+ name: "test-public-channel-ab12c",
287
+ threadId: "thread-1",
288
+ },
289
+ ],
290
+ }),
291
+ ],
292
+ });
293
+
294
+ await runTestRule(rule);
295
+
296
+ expect(mocks.postSpy).toHaveBeenCalledTimes(1);
297
+ expect(payloadOfCall(mocks.postSpy, 0).channelIds).toEqual([
298
+ "19:new-channel@thread.tacv2",
299
+ ]);
300
+ });
301
+
302
+ test("a create-only rule addresses the send with the team the channel was created in", async () => {
303
+ /*
304
+ * existingTeam belongs to the post-to-existing half of the rule and is
305
+ * undefined here, so the send used to go out with no team at all.
306
+ */
307
+ const rule: WorkspaceNotificationRule = makeRule({
308
+ overrides: {
309
+ shouldCreateNewChannel: true,
310
+ shouldPostToExistingChannel: false,
311
+ teamToCreateChannelIn: TEAM_A,
312
+ },
313
+ });
314
+
315
+ const mocks: Mocks = mockDeps({
316
+ rule: rule,
317
+ createdChannels: [
318
+ makeCreatedChannel({
319
+ id: "19:new@thread.tacv2",
320
+ name: "oneuptime-alert-x",
321
+ teamId: TEAM_A,
322
+ }),
323
+ ],
324
+ responses: [makeSendResponse({})],
325
+ });
326
+
327
+ await runTestRule(rule);
328
+
329
+ expect(payloadOfCall(mocks.postSpy, 0).teamId).toBe(TEAM_A);
330
+ });
331
+
332
+ test("the created channel's team wins over a DIFFERENT existingTeam on the same rule", async () => {
333
+ /*
334
+ * Both halves configured, naming two different teams. Addressing the created
335
+ * channel with existingTeam pointed the send at a team that does not own it.
336
+ */
337
+ const rule: WorkspaceNotificationRule = makeRule({
338
+ overrides: {
339
+ shouldCreateNewChannel: true,
340
+ shouldPostToExistingChannel: true,
341
+ teamToCreateChannelIn: TEAM_A,
342
+ existingTeam: TEAM_B,
343
+ existingChannelNames: "general",
344
+ },
345
+ });
346
+
347
+ const mocks: Mocks = mockDeps({
348
+ rule: rule,
349
+ createdChannels: [
350
+ makeCreatedChannel({
351
+ id: "19:new@thread.tacv2",
352
+ name: "oneuptime-alert-x",
353
+ teamId: TEAM_A,
354
+ }),
355
+ ],
356
+ responses: [makeSendResponse({})],
357
+ });
358
+
359
+ await runTestRule(rule);
360
+
361
+ expect(mocks.postSpy).toHaveBeenCalledTimes(2);
362
+ expect(payloadOfCall(mocks.postSpy, 0).teamId).toBe(TEAM_A);
363
+ });
364
+
365
+ test("a create-only rule still creates the channel and invites the tester", async () => {
366
+ const rule: WorkspaceNotificationRule = makeRule({
367
+ overrides: {
368
+ shouldCreateNewChannel: true,
369
+ teamToCreateChannelIn: TEAM_A,
370
+ },
371
+ });
372
+
373
+ const mocks: Mocks = mockDeps({
374
+ rule: rule,
375
+ createdChannels: [
376
+ makeCreatedChannel({
377
+ id: "19:new@thread.tacv2",
378
+ name: "oneuptime-alert-x",
379
+ teamId: TEAM_A,
380
+ }),
381
+ ],
382
+ responses: [makeSendResponse({})],
383
+ });
384
+
385
+ await runTestRule(rule);
386
+
387
+ expect(mocks.createChannelsSpy).toHaveBeenCalledTimes(1);
388
+ expect(mocks.inviteUsersSpy).toHaveBeenCalledTimes(1);
389
+ });
390
+
391
+ test("a failed send to a created channel fails the test instead of reporting success", async () => {
392
+ const rule: WorkspaceNotificationRule = makeRule({
393
+ overrides: {
394
+ shouldCreateNewChannel: true,
395
+ teamToCreateChannelIn: TEAM_A,
396
+ },
397
+ });
398
+
399
+ mockDeps({
400
+ rule: rule,
401
+ createdChannels: [
402
+ makeCreatedChannel({
403
+ id: "19:new@thread.tacv2",
404
+ name: "oneuptime-alert-x",
405
+ teamId: TEAM_A,
406
+ }),
407
+ ],
408
+ responses: [
409
+ makeSendResponse({
410
+ errors: [
411
+ {
412
+ id: "19:new@thread.tacv2",
413
+ name: "oneuptime-alert-x",
414
+ error: "The bot is not part of the conversation roster.",
415
+ },
416
+ ],
417
+ }),
418
+ ],
419
+ });
420
+
421
+ await expect(runTestRule(rule)).rejects.toThrow(
422
+ /Failed to send test message to some channels/,
423
+ );
424
+ });
425
+
426
+ test("a failed send to a created channel is recorded in Notification Logs", async () => {
427
+ const rule: WorkspaceNotificationRule = makeRule({
428
+ overrides: {
429
+ shouldCreateNewChannel: true,
430
+ teamToCreateChannelIn: TEAM_A,
431
+ },
432
+ });
433
+
434
+ const mocks: Mocks = mockDeps({
435
+ rule: rule,
436
+ createdChannels: [
437
+ makeCreatedChannel({
438
+ id: "19:new@thread.tacv2",
439
+ name: "oneuptime-alert-x",
440
+ teamId: TEAM_A,
441
+ }),
442
+ ],
443
+ responses: [
444
+ makeSendResponse({
445
+ errors: [
446
+ {
447
+ id: "19:new@thread.tacv2",
448
+ name: "oneuptime-alert-x",
449
+ error: "boom",
450
+ },
451
+ ],
452
+ }),
453
+ ],
454
+ });
455
+
456
+ await expect(runTestRule(rule)).rejects.toThrow();
457
+ expect(mocks.createLogSpy).toHaveBeenCalled();
458
+ });
459
+
460
+ test("a successful send to a created channel logs a Success entry", async () => {
461
+ const rule: WorkspaceNotificationRule = makeRule({
462
+ overrides: {
463
+ shouldCreateNewChannel: true,
464
+ teamToCreateChannelIn: TEAM_A,
465
+ },
466
+ });
467
+
468
+ const mocks: Mocks = mockDeps({
469
+ rule: rule,
470
+ createdChannels: [
471
+ makeCreatedChannel({
472
+ id: "19:new@thread.tacv2",
473
+ name: "oneuptime-alert-x",
474
+ teamId: TEAM_A,
475
+ }),
476
+ ],
477
+ responses: [
478
+ makeSendResponse({
479
+ threads: [
480
+ {
481
+ id: "19:new@thread.tacv2",
482
+ name: "oneuptime-alert-x",
483
+ threadId: "thread-7",
484
+ },
485
+ ],
486
+ }),
487
+ ],
488
+ });
489
+
490
+ await runTestRule(rule);
491
+
492
+ const log: WorkspaceNotificationLog = mocks.createLogSpy.mock.calls[0]![0]
493
+ .data as WorkspaceNotificationLog;
494
+ expect(log.threadId).toBe("thread-7");
495
+ expect(log.channelName).toBe("oneuptime-alert-x");
496
+ });
497
+
498
+ test("several created channels each get their own send", async () => {
499
+ const rule: WorkspaceNotificationRule = makeRule({
500
+ overrides: {
501
+ shouldCreateNewChannel: true,
502
+ teamToCreateChannelIn: TEAM_A,
503
+ },
504
+ });
505
+
506
+ const mocks: Mocks = mockDeps({
507
+ rule: rule,
508
+ createdChannels: [
509
+ makeCreatedChannel({
510
+ id: "19:a@thread.tacv2",
511
+ name: "a",
512
+ teamId: TEAM_A,
513
+ }),
514
+ makeCreatedChannel({
515
+ id: "19:b@thread.tacv2",
516
+ name: "b",
517
+ teamId: TEAM_A,
518
+ }),
519
+ ],
520
+ responses: [makeSendResponse({})],
521
+ });
522
+
523
+ await runTestRule(rule);
524
+
525
+ expect(mocks.postSpy).toHaveBeenCalledTimes(2);
526
+ expect(payloadOfCall(mocks.postSpy, 0).channelIds).toEqual([
527
+ "19:a@thread.tacv2",
528
+ ]);
529
+ expect(payloadOfCall(mocks.postSpy, 1).channelIds).toEqual([
530
+ "19:b@thread.tacv2",
531
+ ]);
532
+ });
533
+
534
+ test("a rule that creates nothing and posts nowhere sends nothing", async () => {
535
+ // Not every empty rule should now start posting — only created channels did.
536
+ const rule: WorkspaceNotificationRule = makeRule({
537
+ overrides: {
538
+ shouldCreateNewChannel: false,
539
+ shouldPostToExistingChannel: false,
540
+ },
541
+ });
542
+
543
+ const mocks: Mocks = mockDeps({ rule: rule, responses: [] });
544
+
545
+ await runTestRule(rule);
546
+
547
+ expect(mocks.postSpy).not.toHaveBeenCalled();
548
+ });
549
+
550
+ test("Slack create-only rules post too, and carry no teamId", async () => {
551
+ const rule: WorkspaceNotificationRule = makeRule({
552
+ workspaceType: WorkspaceType.Slack,
553
+ overrides: { shouldCreateNewChannel: true },
554
+ });
555
+
556
+ const mocks: Mocks = mockDeps({
557
+ rule: rule,
558
+ createdChannels: [
559
+ makeCreatedChannel({
560
+ id: "C123",
561
+ name: "oneuptime-alert-x",
562
+ workspaceType: WorkspaceType.Slack,
563
+ }),
564
+ ],
565
+ responses: [makeSendResponse({ workspaceType: WorkspaceType.Slack })],
566
+ });
567
+
568
+ await runTestRule(rule);
569
+
570
+ expect(mocks.postSpy).toHaveBeenCalledTimes(1);
571
+ // teamId is a Teams concept and must not be set on a Slack payload.
572
+ expect(payloadOfCall(mocks.postSpy, 0).teamId).toBeUndefined();
573
+ });
574
+ });
575
+
576
+ describe("testRule posting to existing channels is unchanged", () => {
577
+ test("an existing-channel rule posts once per channel with the rule's team", async () => {
578
+ const rule: WorkspaceNotificationRule = makeRule({
579
+ overrides: {
580
+ shouldPostToExistingChannel: true,
581
+ existingTeam: TEAM_B,
582
+ existingChannelNames: "general,test public channel",
583
+ },
584
+ });
585
+
586
+ const mocks: Mocks = mockDeps({
587
+ rule: rule,
588
+ responses: [makeSendResponse({})],
589
+ });
590
+
591
+ await runTestRule(rule);
592
+
593
+ expect(mocks.postSpy).toHaveBeenCalledTimes(2);
594
+ expect(payloadOfCall(mocks.postSpy, 0).channelNames).toEqual(["general"]);
595
+ expect(payloadOfCall(mocks.postSpy, 0).teamId).toBe(TEAM_B);
596
+ expect(payloadOfCall(mocks.postSpy, 1).channelNames).toEqual([
597
+ "test public channel",
598
+ ]);
599
+ });
600
+
601
+ test("an existing channel that does not resolve fails the test before any send", async () => {
602
+ const rule: WorkspaceNotificationRule = makeRule({
603
+ overrides: {
604
+ shouldPostToExistingChannel: true,
605
+ existingTeam: TEAM_B,
606
+ existingChannelNames: "typo-channel",
607
+ },
608
+ });
609
+
610
+ const mocks: Mocks = mockDeps({
611
+ rule: rule,
612
+ channelExists: false,
613
+ responses: [makeSendResponse({})],
614
+ });
615
+
616
+ await expect(runTestRule(rule)).rejects.toThrow(/does not exist/);
617
+ expect(mocks.postSpy).not.toHaveBeenCalled();
618
+ });
619
+
620
+ test("existing channels are validated BEFORE the created-channel post runs", async () => {
621
+ /*
622
+ * Preserved ordering: a rule with both halves and a bad existing-channel
623
+ * name must fail without having posted anything, so the test does not leave
624
+ * a card in a freshly created channel and then report failure.
625
+ */
626
+ const rule: WorkspaceNotificationRule = makeRule({
627
+ overrides: {
628
+ shouldCreateNewChannel: true,
629
+ teamToCreateChannelIn: TEAM_A,
630
+ shouldPostToExistingChannel: true,
631
+ existingTeam: TEAM_B,
632
+ existingChannelNames: "typo-channel",
633
+ },
634
+ });
635
+
636
+ const mocks: Mocks = mockDeps({
637
+ rule: rule,
638
+ createdChannels: [
639
+ makeCreatedChannel({
640
+ id: "19:new@thread.tacv2",
641
+ name: "oneuptime-alert-x",
642
+ teamId: TEAM_A,
643
+ }),
644
+ ],
645
+ channelExists: false,
646
+ responses: [makeSendResponse({})],
647
+ });
648
+
649
+ await expect(runTestRule(rule)).rejects.toThrow(/does not exist/);
650
+ expect(mocks.postSpy).not.toHaveBeenCalled();
651
+ });
652
+
653
+ test("a Microsoft Teams existing-channel rule with no team selected is rejected", async () => {
654
+ const rule: WorkspaceNotificationRule = makeRule({
655
+ overrides: {
656
+ shouldPostToExistingChannel: true,
657
+ existingChannelNames: "general",
658
+ },
659
+ });
660
+
661
+ const mocks: Mocks = mockDeps({
662
+ rule: rule,
663
+ responses: [makeSendResponse({})],
664
+ });
665
+
666
+ await expect(runTestRule(rule)).rejects.toThrow(
667
+ /requires a team to be selected/,
668
+ );
669
+ expect(mocks.postSpy).not.toHaveBeenCalled();
670
+ });
671
+
672
+ test("created channels are posted to before existing ones", async () => {
673
+ const rule: WorkspaceNotificationRule = makeRule({
674
+ overrides: {
675
+ shouldCreateNewChannel: true,
676
+ teamToCreateChannelIn: TEAM_A,
677
+ shouldPostToExistingChannel: true,
678
+ existingTeam: TEAM_B,
679
+ existingChannelNames: "general",
680
+ },
681
+ });
682
+
683
+ const mocks: Mocks = mockDeps({
684
+ rule: rule,
685
+ createdChannels: [
686
+ makeCreatedChannel({
687
+ id: "19:new@thread.tacv2",
688
+ name: "oneuptime-alert-x",
689
+ teamId: TEAM_A,
690
+ }),
691
+ ],
692
+ responses: [makeSendResponse({})],
693
+ });
694
+
695
+ await runTestRule(rule);
696
+
697
+ expect(payloadOfCall(mocks.postSpy, 0).channelIds).toEqual([
698
+ "19:new@thread.tacv2",
699
+ ]);
700
+ expect(payloadOfCall(mocks.postSpy, 1).channelNames).toEqual(["general"]);
701
+ });
702
+ });