@oneuptime/common 11.5.13 → 11.6.1

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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -0,0 +1,564 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import NotificationMethodUtil, {
3
+ ColumnValueReader,
4
+ NotificationMethodDisplayItem,
5
+ NotificationMethodModels,
6
+ } from "../../../UI/Utils/NotificationMethodUtil";
7
+ import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
8
+ import UserCall from "../../../Models/DatabaseModels/UserCall";
9
+ import UserEmail from "../../../Models/DatabaseModels/UserEmail";
10
+ import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
11
+ import UserPush from "../../../Models/DatabaseModels/UserPush";
12
+ import UserSMS from "../../../Models/DatabaseModels/UserSMS";
13
+ import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
14
+ import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
15
+ import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
16
+ import Email from "../../../Types/Email";
17
+ import ObjectID from "../../../Types/ObjectID";
18
+ import Phone from "../../../Types/Phone";
19
+
20
+ /*
21
+ * These tests pin down the notification-method logic that used to live,
22
+ * copy-pasted, inside four on-call rule page components. The value of pulling it
23
+ * into Common/UI/Utils is that the behavior below is now asserted once instead
24
+ * of being invisible inside React closures.
25
+ */
26
+
27
+ // -------------------------------------------------------------- model factories
28
+
29
+ function webhook(id: string, name?: string, url?: string): UserWebhook {
30
+ const model: UserWebhook = new UserWebhook();
31
+ model.id = new ObjectID(id);
32
+ if (name !== undefined) {
33
+ model.name = name;
34
+ }
35
+ if (url !== undefined) {
36
+ model.webhookUrl = url;
37
+ }
38
+ return model;
39
+ }
40
+
41
+ function email(id: string, address?: string): UserEmail {
42
+ const model: UserEmail = new UserEmail();
43
+ model.id = new ObjectID(id);
44
+ if (address !== undefined) {
45
+ model.email = new Email(address);
46
+ }
47
+ return model;
48
+ }
49
+
50
+ function sms(id: string, phone?: string): UserSMS {
51
+ const model: UserSMS = new UserSMS();
52
+ model.id = new ObjectID(id);
53
+ if (phone !== undefined) {
54
+ model.phone = new Phone(phone);
55
+ }
56
+ return model;
57
+ }
58
+
59
+ function call(id: string, phone?: string): UserCall {
60
+ const model: UserCall = new UserCall();
61
+ model.id = new ObjectID(id);
62
+ if (phone !== undefined) {
63
+ model.phone = new Phone(phone);
64
+ }
65
+ return model;
66
+ }
67
+
68
+ function whatsApp(id: string, phone?: string): UserWhatsApp {
69
+ const model: UserWhatsApp = new UserWhatsApp();
70
+ model.id = new ObjectID(id);
71
+ if (phone !== undefined) {
72
+ model.phone = new Phone(phone);
73
+ }
74
+ return model;
75
+ }
76
+
77
+ function telegram(id: string, handle?: string, chatId?: string): UserTelegram {
78
+ const model: UserTelegram = new UserTelegram();
79
+ model.id = new ObjectID(id);
80
+ if (handle !== undefined) {
81
+ model.telegramUserHandle = handle;
82
+ }
83
+ if (chatId !== undefined) {
84
+ model.telegramChatId = chatId;
85
+ }
86
+ return model;
87
+ }
88
+
89
+ function push(id: string, deviceName?: string): UserPush {
90
+ const model: UserPush = new UserPush();
91
+ model.id = new ObjectID(id);
92
+ if (deviceName !== undefined) {
93
+ model.deviceName = deviceName;
94
+ }
95
+ return model;
96
+ }
97
+
98
+ function emptyModels(): NotificationMethodModels {
99
+ return {
100
+ userCalls: [],
101
+ userEmails: [],
102
+ userSMSs: [],
103
+ userPush: [],
104
+ userWhatsApps: [],
105
+ userTelegrams: [],
106
+ userWebhooks: [],
107
+ };
108
+ }
109
+
110
+ // A 24-hex-char string is a valid ObjectID.
111
+ const ID: Record<"a" | "b" | "c" | "d" | "e" | "f" | "g", string> = {
112
+ a: "aaaaaaaaaaaaaaaaaaaaaaaa",
113
+ b: "bbbbbbbbbbbbbbbbbbbbbbbb",
114
+ c: "cccccccccccccccccccccccc",
115
+ d: "dddddddddddddddddddddddd",
116
+ e: "eeeeeeeeeeeeeeeeeeeeeeee",
117
+ f: "ffffffffffffffffffffffff",
118
+ g: "abcabcabcabcabcabcabcabc",
119
+ };
120
+
121
+ /*
122
+ * A minimal stand-in for a model row: getDisplayItems only needs getColumnValue,
123
+ * so a plain map exercises the same code path a real model would.
124
+ */
125
+ function reader(columns: Record<string, unknown>): ColumnValueReader {
126
+ return {
127
+ getColumnValue: (columnName: string): unknown => {
128
+ return columnName in columns ? columns[columnName] : null;
129
+ },
130
+ };
131
+ }
132
+
133
+ // ================================================ getSelectForNotificationMethods
134
+
135
+ describe("NotificationMethodUtil.getSelectForNotificationMethods", () => {
136
+ test("selects every notification-method relation", () => {
137
+ const select: Record<string, unknown> =
138
+ NotificationMethodUtil.getSelectForNotificationMethods<UserNotificationRule>() as Record<
139
+ string,
140
+ unknown
141
+ >;
142
+
143
+ expect(Object.keys(select).sort()).toEqual(
144
+ [
145
+ "userCall",
146
+ "userEmail",
147
+ "userPush",
148
+ "userSms",
149
+ "userTelegram",
150
+ "userWebhook",
151
+ "userWhatsApp",
152
+ ].sort(),
153
+ );
154
+ });
155
+
156
+ test("selects the identifier column for each simple method", () => {
157
+ const select: Record<
158
+ string,
159
+ Record<string, boolean>
160
+ > = NotificationMethodUtil.getSelectForNotificationMethods<UserNotificationRule>() as Record<
161
+ string,
162
+ Record<string, boolean>
163
+ >;
164
+
165
+ expect(select["userEmail"]).toEqual({ email: true });
166
+ expect(select["userCall"]).toEqual({ phone: true });
167
+ expect(select["userSms"]).toEqual({ phone: true });
168
+ expect(select["userWhatsApp"]).toEqual({ phone: true });
169
+ expect(select["userPush"]).toEqual({ deviceName: true });
170
+ expect(select["userTelegram"]).toEqual({
171
+ telegramUserHandle: true,
172
+ telegramChatId: true,
173
+ });
174
+ });
175
+
176
+ test("selects ONLY name for a webhook — never the credential-bearing URL or secret", () => {
177
+ const select: Record<
178
+ string,
179
+ Record<string, boolean>
180
+ > = NotificationMethodUtil.getSelectForNotificationMethods<UserNotificationRule>() as Record<
181
+ string,
182
+ Record<string, boolean>
183
+ >;
184
+
185
+ expect(select["userWebhook"]).toEqual({ name: true });
186
+ expect(select["userWebhook"]).not.toHaveProperty("webhookUrl");
187
+ expect(select["userWebhook"]).not.toHaveProperty("secret");
188
+ });
189
+
190
+ test("returns a fresh object each call so callers cannot mutate shared state", () => {
191
+ const first: Record<
192
+ string,
193
+ Record<string, boolean>
194
+ > = NotificationMethodUtil.getSelectForNotificationMethods<UserNotificationRule>() as Record<
195
+ string,
196
+ Record<string, boolean>
197
+ >;
198
+
199
+ first["userWebhook"]!["webhookUrl"] = true;
200
+
201
+ const second: Record<
202
+ string,
203
+ Record<string, boolean>
204
+ > = NotificationMethodUtil.getSelectForNotificationMethods<UserNotificationRule>() as Record<
205
+ string,
206
+ Record<string, boolean>
207
+ >;
208
+
209
+ expect(second["userWebhook"]).toEqual({ name: true });
210
+ });
211
+ });
212
+
213
+ // ============================================================== getDisplayItems
214
+
215
+ describe("NotificationMethodUtil.getDisplayItems", () => {
216
+ test("returns an empty array when no relation is populated", () => {
217
+ expect(NotificationMethodUtil.getDisplayItems(reader({}))).toEqual([]);
218
+ });
219
+
220
+ test("renders an email line", () => {
221
+ const items: Array<NotificationMethodDisplayItem> =
222
+ NotificationMethodUtil.getDisplayItems(
223
+ reader({ userEmail: { email: "jane@example.com" } }),
224
+ );
225
+
226
+ expect(items).toEqual([{ title: "Email", value: "jane@example.com" }]);
227
+ });
228
+
229
+ test("renders a webhook line using its name", () => {
230
+ const items: Array<NotificationMethodDisplayItem> =
231
+ NotificationMethodUtil.getDisplayItems(
232
+ reader({ userWebhook: { name: "Deploy Hook" } }),
233
+ );
234
+
235
+ expect(items).toEqual([{ title: "Webhook", value: "Deploy Hook" }]);
236
+ });
237
+
238
+ test("never renders the webhook URL, even when the relation object carries it", () => {
239
+ const items: Array<NotificationMethodDisplayItem> =
240
+ NotificationMethodUtil.getDisplayItems(
241
+ reader({
242
+ userWebhook: {
243
+ name: "Deploy Hook",
244
+ webhookUrl: "https://hooks.slack.com/services/T/B/SECRETTOKEN",
245
+ },
246
+ }),
247
+ );
248
+
249
+ expect(items).toEqual([{ title: "Webhook", value: "Deploy Hook" }]);
250
+ expect(JSON.stringify(items)).not.toContain("SECRETTOKEN");
251
+ });
252
+
253
+ test("renders no line for a webhook that has only a URL and no name", () => {
254
+ const items: Array<NotificationMethodDisplayItem> =
255
+ NotificationMethodUtil.getDisplayItems(
256
+ reader({
257
+ userWebhook: {
258
+ webhookUrl: "https://hooks.slack.com/services/T/B/SECRETTOKEN",
259
+ },
260
+ }),
261
+ );
262
+
263
+ expect(items).toEqual([]);
264
+ expect(JSON.stringify(items)).not.toContain("SECRETTOKEN");
265
+ });
266
+
267
+ test("prefers the Telegram handle over the chat id", () => {
268
+ const items: Array<NotificationMethodDisplayItem> =
269
+ NotificationMethodUtil.getDisplayItems(
270
+ reader({
271
+ userTelegram: { telegramUserHandle: "@jane", telegramChatId: "999" },
272
+ }),
273
+ );
274
+
275
+ expect(items).toEqual([{ title: "Telegram", value: "@jane" }]);
276
+ });
277
+
278
+ test("falls back to the Telegram chat id when the handle is absent", () => {
279
+ const items: Array<NotificationMethodDisplayItem> =
280
+ NotificationMethodUtil.getDisplayItems(
281
+ reader({ userTelegram: { telegramChatId: "999" } }),
282
+ );
283
+
284
+ expect(items).toEqual([{ title: "Telegram", value: "999" }]);
285
+ });
286
+
287
+ test("ignores a relation whose value is not an object", () => {
288
+ expect(
289
+ NotificationMethodUtil.getDisplayItems(
290
+ reader({ userEmail: "not-an-object" }),
291
+ ),
292
+ ).toEqual([]);
293
+ });
294
+
295
+ test("ignores an empty-string identifier", () => {
296
+ expect(
297
+ NotificationMethodUtil.getDisplayItems(
298
+ reader({ userEmail: { email: "" } }),
299
+ ),
300
+ ).toEqual([]);
301
+ });
302
+
303
+ test("renders lines in a stable method order", () => {
304
+ const items: Array<NotificationMethodDisplayItem> =
305
+ NotificationMethodUtil.getDisplayItems(
306
+ reader({
307
+ userPush: { deviceName: "Pixel" },
308
+ userEmail: { email: "jane@example.com" },
309
+ userWebhook: { name: "Hook" },
310
+ }),
311
+ );
312
+
313
+ expect(items).toEqual([
314
+ { title: "Email", value: "jane@example.com" },
315
+ { title: "Webhook", value: "Hook" },
316
+ { title: "Push", value: "Pixel" },
317
+ ]);
318
+ });
319
+
320
+ test("works with a real UserNotificationRule instance", () => {
321
+ const rule: UserNotificationRule = new UserNotificationRule();
322
+ rule.setColumnValue("userWebhook", { name: "Deploy Hook" });
323
+
324
+ expect(NotificationMethodUtil.getDisplayItems(rule)).toEqual([
325
+ { title: "Webhook", value: "Deploy Hook" },
326
+ ]);
327
+ });
328
+ });
329
+
330
+ // ==================================================================== getLabel
331
+
332
+ describe("NotificationMethodUtil.getLabel", () => {
333
+ test("labels a webhook with its name", () => {
334
+ expect(NotificationMethodUtil.getLabel(webhook(ID.a, "Deploy Hook"))).toBe(
335
+ "Webhook: Deploy Hook",
336
+ );
337
+ });
338
+
339
+ test("labels a nameless webhook as Unknown Webhook — never its URL", () => {
340
+ const model: UserWebhook = webhook(
341
+ ID.a,
342
+ "",
343
+ "https://hooks.slack.com/services/T/B/SECRETTOKEN",
344
+ );
345
+
346
+ const label: string = NotificationMethodUtil.getLabel(model);
347
+
348
+ expect(label).toBe("Webhook: Unknown Webhook");
349
+ expect(label).not.toContain("SECRETTOKEN");
350
+ });
351
+
352
+ test("labels a push device, falling back to Unknown Device", () => {
353
+ expect(NotificationMethodUtil.getLabel(push(ID.a, "Pixel 8"))).toBe(
354
+ "Push: Pixel 8",
355
+ );
356
+ expect(NotificationMethodUtil.getLabel(push(ID.b))).toBe(
357
+ "Push: Unknown Device",
358
+ );
359
+ });
360
+
361
+ test("labels a Telegram method by handle, then chat id, then Unknown Chat", () => {
362
+ expect(NotificationMethodUtil.getLabel(telegram(ID.a, "@jane"))).toBe(
363
+ "Telegram: @jane",
364
+ );
365
+ expect(
366
+ NotificationMethodUtil.getLabel(telegram(ID.b, undefined, "999")),
367
+ ).toBe("Telegram: 999");
368
+ expect(NotificationMethodUtil.getLabel(telegram(ID.c))).toBe(
369
+ "Telegram: Unknown Chat",
370
+ );
371
+ });
372
+
373
+ test("labels call, sms, whatsapp with the phone number and their prefix", () => {
374
+ expect(NotificationMethodUtil.getLabel(call(ID.a, "+15555550100"))).toBe(
375
+ "Call: +15555550100",
376
+ );
377
+ expect(NotificationMethodUtil.getLabel(sms(ID.b, "+15555550101"))).toBe(
378
+ "SMS: +15555550101",
379
+ );
380
+ expect(
381
+ NotificationMethodUtil.getLabel(whatsApp(ID.c, "+15555550102")),
382
+ ).toBe("WhatsApp: +15555550102");
383
+ });
384
+
385
+ test("labels an email method with the address", () => {
386
+ expect(
387
+ NotificationMethodUtil.getLabel(email(ID.a, "jane@example.com")),
388
+ ).toBe("Email: jane@example.com");
389
+ });
390
+ });
391
+
392
+ // ========================================================== getDropdownOptions
393
+
394
+ describe("NotificationMethodUtil.getDropdownOptions", () => {
395
+ test("returns no options when the user has configured no methods", () => {
396
+ expect(NotificationMethodUtil.getDropdownOptions(emptyModels())).toEqual(
397
+ [],
398
+ );
399
+ });
400
+
401
+ test("creates one option per configured method, value = model id", () => {
402
+ const models: NotificationMethodModels = {
403
+ ...emptyModels(),
404
+ userWebhooks: [webhook(ID.a, "Deploy Hook")],
405
+ };
406
+
407
+ const options: Array<DropdownOption> =
408
+ NotificationMethodUtil.getDropdownOptions(models);
409
+
410
+ expect(options).toEqual([{ label: "Webhook: Deploy Hook", value: ID.a }]);
411
+ });
412
+
413
+ test("emits options grouped by type in a stable order", () => {
414
+ const models: NotificationMethodModels = {
415
+ userCalls: [call(ID.a, "+15555550100")],
416
+ userEmails: [email(ID.b, "jane@example.com")],
417
+ userSMSs: [sms(ID.c, "+15555550101")],
418
+ userPush: [push(ID.d, "Pixel")],
419
+ userWhatsApps: [whatsApp(ID.e, "+15555550102")],
420
+ userTelegrams: [telegram(ID.f, "@jane")],
421
+ userWebhooks: [webhook(ID.g, "Hook")],
422
+ };
423
+
424
+ const labels: Array<string> = NotificationMethodUtil.getDropdownOptions(
425
+ models,
426
+ ).map((option: DropdownOption) => {
427
+ return option.label;
428
+ });
429
+
430
+ expect(labels).toEqual([
431
+ "Call: +15555550100",
432
+ "Email: jane@example.com",
433
+ "SMS: +15555550101",
434
+ "Push: Pixel",
435
+ "WhatsApp: +15555550102",
436
+ "Telegram: @jane",
437
+ "Webhook: Hook",
438
+ ]);
439
+ });
440
+
441
+ test("keeps multiple methods of the same type", () => {
442
+ const models: NotificationMethodModels = {
443
+ ...emptyModels(),
444
+ userWebhooks: [webhook(ID.a, "Hook A"), webhook(ID.b, "Hook B")],
445
+ };
446
+
447
+ const options: Array<DropdownOption> =
448
+ NotificationMethodUtil.getDropdownOptions(models);
449
+
450
+ expect(options).toEqual([
451
+ { label: "Webhook: Hook A", value: ID.a },
452
+ { label: "Webhook: Hook B", value: ID.b },
453
+ ]);
454
+ });
455
+ });
456
+
457
+ // ====================================================== setSelectedMethodOnRule
458
+
459
+ describe("NotificationMethodUtil.setSelectedMethodOnRule", () => {
460
+ function models(): NotificationMethodModels {
461
+ return {
462
+ userCalls: [call(ID.a, "+15555550100")],
463
+ userEmails: [email(ID.b, "jane@example.com")],
464
+ userSMSs: [sms(ID.c, "+15555550101")],
465
+ userPush: [push(ID.d, "Pixel")],
466
+ userWhatsApps: [whatsApp(ID.e, "+15555550102")],
467
+ userTelegrams: [telegram(ID.f, "@jane")],
468
+ userWebhooks: [webhook(ID.g, "Hook")],
469
+ };
470
+ }
471
+
472
+ test("sets userWebhookId when a webhook is selected", () => {
473
+ const rule: UserNotificationRule = new UserNotificationRule();
474
+
475
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, ID.g, models());
476
+
477
+ expect(rule.userWebhookId?.toString()).toBe(ID.g);
478
+ });
479
+
480
+ test("sets only the one foreign key that matches the selection", () => {
481
+ const rule: UserNotificationRule = new UserNotificationRule();
482
+
483
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, ID.g, models());
484
+
485
+ expect(rule.userWebhookId?.toString()).toBe(ID.g);
486
+ expect(rule.userEmailId).toBeUndefined();
487
+ expect(rule.userSmsId).toBeUndefined();
488
+ expect(rule.userCallId).toBeUndefined();
489
+ expect(rule.userPushId).toBeUndefined();
490
+ expect(rule.userWhatsAppId).toBeUndefined();
491
+ expect(rule.userTelegramId).toBeUndefined();
492
+ });
493
+
494
+ test("maps each method type to its own foreign key", () => {
495
+ const cases: Array<{ id: string; key: keyof UserNotificationRule }> = [
496
+ { id: ID.a, key: "userCallId" },
497
+ { id: ID.b, key: "userEmailId" },
498
+ { id: ID.c, key: "userSmsId" },
499
+ { id: ID.d, key: "userPushId" },
500
+ { id: ID.e, key: "userWhatsAppId" },
501
+ { id: ID.f, key: "userTelegramId" },
502
+ { id: ID.g, key: "userWebhookId" },
503
+ ];
504
+
505
+ for (const testCase of cases) {
506
+ const rule: UserNotificationRule = new UserNotificationRule();
507
+
508
+ NotificationMethodUtil.setSelectedMethodOnRule(
509
+ rule,
510
+ testCase.id,
511
+ models(),
512
+ );
513
+
514
+ expect((rule[testCase.key] as ObjectID | undefined)?.toString()).toBe(
515
+ testCase.id,
516
+ );
517
+ }
518
+ });
519
+
520
+ test("leaves the rule untouched when the selection matches nothing", () => {
521
+ const rule: UserNotificationRule = new UserNotificationRule();
522
+
523
+ NotificationMethodUtil.setSelectedMethodOnRule(
524
+ rule,
525
+ "999999999999999999999999",
526
+ models(),
527
+ );
528
+
529
+ expect(rule.userWebhookId).toBeUndefined();
530
+ expect(rule.userEmailId).toBeUndefined();
531
+ });
532
+
533
+ test("does nothing when no method is selected", () => {
534
+ const rule: UserNotificationRule = new UserNotificationRule();
535
+
536
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, undefined, models());
537
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, null, models());
538
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, "", models());
539
+
540
+ expect(rule.userWebhookId).toBeUndefined();
541
+ expect(rule.userEmailId).toBeUndefined();
542
+ });
543
+
544
+ test("accepts an ObjectID selection value, not just a string", () => {
545
+ const rule: UserNotificationRule = new UserNotificationRule();
546
+
547
+ NotificationMethodUtil.setSelectedMethodOnRule(
548
+ rule,
549
+ new ObjectID(ID.g),
550
+ models(),
551
+ );
552
+
553
+ expect(rule.userWebhookId?.toString()).toBe(ID.g);
554
+ });
555
+
556
+ test("returns the same rule instance it was given", () => {
557
+ const rule: UserNotificationRule = new UserNotificationRule();
558
+
559
+ const returned: UserNotificationRule =
560
+ NotificationMethodUtil.setSelectedMethodOnRule(rule, ID.b, models());
561
+
562
+ expect(returned).toBe(rule);
563
+ });
564
+ });