@tellescope/sdk 1.255.7 → 1.255.9

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 (41) hide show
  1. package/lib/cjs/enduser.d.ts +1 -0
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/sdk.d.ts +1 -0
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/session.d.ts +1 -0
  6. package/lib/cjs/session.d.ts.map +1 -1
  7. package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  8. package/lib/cjs/tests/api_tests/custom_dashboards.test.js +258 -32
  9. package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
  10. package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts +6 -0
  11. package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts.map +1 -0
  12. package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.js +183 -0
  13. package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.js.map +1 -0
  14. package/lib/cjs/tests/api_tests/webhook_error_handling.test.d.ts +6 -0
  15. package/lib/cjs/tests/api_tests/webhook_error_handling.test.d.ts.map +1 -0
  16. package/lib/cjs/tests/api_tests/webhook_error_handling.test.js +324 -0
  17. package/lib/cjs/tests/api_tests/webhook_error_handling.test.js.map +1 -0
  18. package/lib/cjs/tests/tests.d.ts.map +1 -1
  19. package/lib/cjs/tests/tests.js +172 -168
  20. package/lib/cjs/tests/tests.js.map +1 -1
  21. package/lib/esm/sdk.d.ts +2 -2
  22. package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  23. package/lib/esm/tests/api_tests/custom_dashboards.test.js +259 -33
  24. package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
  25. package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts +6 -0
  26. package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts.map +1 -0
  27. package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.js +179 -0
  28. package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.js.map +1 -0
  29. package/lib/esm/tests/api_tests/webhook_error_handling.test.d.ts +6 -0
  30. package/lib/esm/tests/api_tests/webhook_error_handling.test.d.ts.map +1 -0
  31. package/lib/esm/tests/api_tests/webhook_error_handling.test.js +317 -0
  32. package/lib/esm/tests/api_tests/webhook_error_handling.test.js.map +1 -0
  33. package/lib/esm/tests/tests.d.ts.map +1 -1
  34. package/lib/esm/tests/tests.js +172 -168
  35. package/lib/esm/tests/tests.js.map +1 -1
  36. package/lib/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +8 -8
  38. package/src/tests/api_tests/custom_dashboards.test.ts +208 -0
  39. package/src/tests/api_tests/webhook_error_handling.test.ts +190 -0
  40. package/src/tests/tests.ts +3 -1
  41. package/test_generated.pdf +0 -0
@@ -4,6 +4,7 @@ import { Session } from "../../sdk"
4
4
  import {
5
5
  async_test,
6
6
  log_header,
7
+ wait,
7
8
  } from "@tellescope/testing"
8
9
  import { setup_tests } from "../setup"
9
10
 
@@ -199,6 +200,56 @@ export const custom_dashboards_tests = async ({ sdk, sdkNonAdmin }: { sdk: Sessi
199
200
  }
200
201
  )
201
202
 
203
+ // Test 6b: Create dashboard with every built-in block type (legacy renderer set)
204
+ const dashboardWithBuiltInTypes = await sdk.api.custom_dashboards.createOne({
205
+ title: "Built-In Block Types Dashboard",
206
+ blocks: [
207
+ { type: "Inbox" },
208
+ { type: "Tickets" },
209
+ { type: "Team Chats" },
210
+ { type: "Upcoming Events" },
211
+ { type: "To-Dos" },
212
+ { type: "Database", info: { databaseId: "60398b0231a295e64f084fd9" } },
213
+ ],
214
+ })
215
+
216
+ await async_test(
217
+ "Dashboard with all built-in block types created correctly",
218
+ async () => dashboardWithBuiltInTypes,
219
+ {
220
+ onResult: r => (
221
+ r.blocks.length === 6
222
+ && r.blocks[0].type === "Inbox"
223
+ && r.blocks[1].type === "Tickets"
224
+ && r.blocks[2].type === "Team Chats"
225
+ && r.blocks[3].type === "Upcoming Events"
226
+ && r.blocks[4].type === "To-Dos"
227
+ && r.blocks[5].type === "Database"
228
+ && r.blocks[5].info !== undefined && r.blocks[5].info.databaseId === "60398b0231a295e64f084fd9"
229
+ )
230
+ }
231
+ )
232
+
233
+ // Test 6c: Unknown top-level block fields are stripped on save (params belong in info)
234
+ const dashboardWithUnknownBlockField = await sdk.api.custom_dashboards.createOne({
235
+ title: "Unknown Block Field Dashboard",
236
+ blocks: [
237
+ { type: "Inbox", customTopLevel: "x", info: { kept: true } } as any,
238
+ ],
239
+ })
240
+
241
+ await async_test(
242
+ "Unknown top-level block fields stripped, info preserved",
243
+ async () => dashboardWithUnknownBlockField,
244
+ {
245
+ onResult: r => (
246
+ r.blocks.length === 1
247
+ && (r.blocks[0] as any).customTopLevel === undefined
248
+ && r.blocks[0].info !== undefined && r.blocks[0].info.kept === true
249
+ )
250
+ }
251
+ )
252
+
202
253
  // Test 7: Update gridConfig only
203
254
  const updatedGridConfig = await sdk.api.custom_dashboards.updateOne(dashboardWithNewTypes.id, {
204
255
  gridConfig: { columns: 24, gap: 8 },
@@ -215,6 +266,67 @@ export const custom_dashboards_tests = async ({ sdk, sdkNonAdmin }: { sdk: Sessi
215
266
  }
216
267
  )
217
268
 
269
+ // Test 7b: Create dashboard with top-level type field
270
+ const typedDashboard = await sdk.api.custom_dashboards.createOne({
271
+ title: "Typed Dashboard",
272
+ type: "home",
273
+ blocks: [{ type: "Inbox", colSpan: 12 }],
274
+ })
275
+
276
+ await async_test(
277
+ "Dashboard with top-level type created correctly",
278
+ async () => typedDashboard,
279
+ { onResult: r => r.type === "home" }
280
+ )
281
+
282
+ // Test 7c: Dashboards without type still work (backwards compatibility)
283
+ await async_test(
284
+ "Dashboard without type has undefined type",
285
+ async () => sdk.api.custom_dashboards.getOne(basicDashboard.id),
286
+ { onResult: r => r.type === undefined }
287
+ )
288
+
289
+ // Test 7d: Update top-level type
290
+ const updatedType = await sdk.api.custom_dashboards.updateOne(typedDashboard.id, {
291
+ type: "clinical",
292
+ })
293
+
294
+ await async_test(
295
+ "Dashboard type updated correctly",
296
+ async () => updatedType,
297
+ { onResult: r => r.type === "clinical" }
298
+ )
299
+
300
+ // Test 7e: Filter dashboards by type
301
+ const secondTypedDashboard = await sdk.api.custom_dashboards.createOne({
302
+ title: "Second Typed Dashboard",
303
+ type: "clinical",
304
+ blocks: [{ type: "Tickets", colSpan: 12 }],
305
+ })
306
+
307
+ await async_test(
308
+ "Dashboards filtered by type via filter",
309
+ async () => sdk.api.custom_dashboards.getSome({ filter: { type: "clinical" } }),
310
+ {
311
+ onResult: r => (
312
+ r.length === 2
313
+ && r.every(d => d.type === "clinical")
314
+ )
315
+ }
316
+ )
317
+
318
+ // Test 7f: Filter dashboards by type via mdbFilter
319
+ await async_test(
320
+ "Dashboards filtered by type via mdbFilter",
321
+ async () => sdk.api.custom_dashboards.getSome({ mdbFilter: { type: "clinical" } }),
322
+ {
323
+ onResult: r => (
324
+ r.length === 2
325
+ && r.every(d => d.type === "clinical")
326
+ )
327
+ }
328
+ )
329
+
218
330
  // Test 8: Non-admin can read all dashboards by default
219
331
  const nonAdminList = await sdkNonAdmin.api.custom_dashboards.getSome({ filter: {} })
220
332
 
@@ -224,14 +336,110 @@ export const custom_dashboards_tests = async ({ sdk, sdkNonAdmin }: { sdk: Sessi
224
336
  { onResult: r => r.length >= 2 }
225
337
  )
226
338
 
339
+ // Test 9: visibleToAllUsers grants org-wide access to users without full read access
340
+ const restrictedRole = await sdk.api.role_based_access_permissions.createOne({
341
+ role: 'Dashboard Default Access',
342
+ permissions: {
343
+ custom_dashboards: { read: 'Default', create: null, update: null, delete: null },
344
+ },
345
+ })
346
+
347
+ const restrictedUserEmail = 'dashboard.restricted.test@tellescope.com'
348
+ let restrictedUser = await sdk.api.users.getOne({ email: restrictedUserEmail }).catch(() => null) // throws error on none found
349
+ if (restrictedUser && !restrictedUser.verifiedEmail) { // verifiedEmail can only be set on create, so recreate stale users
350
+ await sdk.api.users.deleteOne(restrictedUser.id)
351
+ restrictedUser = null
352
+ }
353
+ if (!restrictedUser) {
354
+ restrictedUser = await sdk.api.users.createOne({ email: restrictedUserEmail, notificationEmailsDisabled: true, verifiedEmail: true })
355
+ }
356
+ // ensure role is set, in case GET returned a user without a role or with a different role
357
+ await sdk.api.users.updateOne(restrictedUser.id, { roles: [restrictedRole.role] }, { replaceObjectFields: true })
358
+ await wait(undefined, 2000) // role change triggers a logout
359
+
360
+ const sdkRestricted = new Session({
361
+ host,
362
+ authToken: (await sdk.api.users.generate_auth_token({ id: restrictedUser.id })).authToken,
363
+ })
364
+ await async_test('test_authenticated (restricted dashboard user)', sdkRestricted.test_authenticated, { expectedResult: 'Authenticated!' })
365
+
366
+ const hiddenDashboard = await sdk.api.custom_dashboards.createOne({
367
+ title: "Hidden From Restricted",
368
+ blocks: [{ type: "Inbox", colSpan: 12 }],
369
+ })
370
+ const orgWideDashboard = await sdk.api.custom_dashboards.createOne({
371
+ title: "Org-Wide Dashboard",
372
+ visibleToAllUsers: true,
373
+ blocks: [{ type: "Inbox", colSpan: 12 }],
374
+ })
375
+ const explicitFalseDashboard = await sdk.api.custom_dashboards.createOne({
376
+ title: "Explicit False Dashboard",
377
+ visibleToAllUsers: false,
378
+ blocks: [{ type: "Inbox", colSpan: 12 }],
379
+ })
380
+ const assignedDashboard = await sdk.api.custom_dashboards.createOne({
381
+ title: "Assigned To Restricted",
382
+ userIds: [restrictedUser.id],
383
+ blocks: [{ type: "Inbox", colSpan: 12 }],
384
+ })
385
+
386
+ await async_test(
387
+ "Restricted user sees org-wide and assigned dashboards only",
388
+ () => sdkRestricted.api.custom_dashboards.getSome({ filter: {} }),
389
+ {
390
+ onResult: r => (
391
+ !!r.find(d => d.id === orgWideDashboard.id)
392
+ && !!r.find(d => d.id === assignedDashboard.id)
393
+ && !r.find(d => d.id === hiddenDashboard.id)
394
+ && !r.find(d => d.id === explicitFalseDashboard.id)
395
+ )
396
+ }
397
+ )
398
+
399
+ await async_test(
400
+ "Restricted user can read org-wide dashboard by id",
401
+ () => sdkRestricted.api.custom_dashboards.getOne(orgWideDashboard.id),
402
+ { onResult: r => r.id === orgWideDashboard.id && r.visibleToAllUsers === true }
403
+ )
404
+
405
+ await async_test(
406
+ "Restricted user cannot read unshared dashboard by id",
407
+ () => sdkRestricted.api.custom_dashboards.getOne(hiddenDashboard.id),
408
+ { shouldError: true, onError: () => true }
409
+ )
410
+
411
+ // Toggling the flag on takes effect for the restricted user
412
+ await sdk.api.custom_dashboards.updateOne(hiddenDashboard.id, { visibleToAllUsers: true })
413
+
414
+ await async_test(
415
+ "Restricted user can read dashboard after visibleToAllUsers enabled",
416
+ () => sdkRestricted.api.custom_dashboards.getOne(hiddenDashboard.id),
417
+ { onResult: r => r.id === hiddenDashboard.id }
418
+ )
419
+
420
+ // Admin continues to see everything, including unshared dashboards
421
+ await async_test(
422
+ "Admin still sees explicit-false dashboard",
423
+ () => sdk.api.custom_dashboards.getOne(explicitFalseDashboard.id),
424
+ { onResult: r => r.id === explicitFalseDashboard.id }
425
+ )
426
+
227
427
  // Cleanup
228
428
  await Promise.all([
429
+ sdk.api.custom_dashboards.deleteOne(hiddenDashboard.id),
430
+ sdk.api.custom_dashboards.deleteOne(orgWideDashboard.id),
431
+ sdk.api.custom_dashboards.deleteOne(explicitFalseDashboard.id),
432
+ sdk.api.custom_dashboards.deleteOne(assignedDashboard.id),
229
433
  sdk.api.custom_dashboards.deleteOne(basicDashboard.id),
230
434
  sdk.api.custom_dashboards.deleteOne(duplicateTitleDashboard.id),
231
435
  sdk.api.custom_dashboards.deleteOne(fullDashboard.id),
232
436
  sdk.api.custom_dashboards.deleteOne(dashboardWithUserIds.id),
233
437
  sdk.api.custom_dashboards.deleteOne(dashboardWithNewTypes.id),
234
438
  sdk.api.custom_dashboards.deleteOne(dashboardWithComplexBlocks.id),
439
+ sdk.api.custom_dashboards.deleteOne(dashboardWithBuiltInTypes.id),
440
+ sdk.api.custom_dashboards.deleteOne(dashboardWithUnknownBlockField.id),
441
+ sdk.api.custom_dashboards.deleteOne(typedDashboard.id),
442
+ sdk.api.custom_dashboards.deleteOne(secondTypedDashboard.id),
235
443
  ])
236
444
  }
237
445
 
@@ -0,0 +1,190 @@
1
+ require('source-map-support').install();
2
+
3
+ import express from "express"
4
+ import bodyParser from "body-parser"
5
+ import crypto from "crypto"
6
+ import http from "http"
7
+
8
+ import { Session } from "../../sdk"
9
+ import {
10
+ assert,
11
+ async_test,
12
+ log_header,
13
+ wait,
14
+ } from "@tellescope/testing"
15
+ import { setup_tests } from "../setup"
16
+
17
+ const host = process.env.API_URL || 'http://localhost:8080' as const
18
+
19
+ const TEST_SECRET = "this is a test secret for verifying integrity of web hooks"
20
+ const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex')
21
+
22
+ // DNS resolution for the reserved .invalid TLD always fails => axios ENOTFOUND (no err.response)
23
+ const ENOTFOUND_URL = `http://webhook-error-handling-test.invalid/hook`
24
+
25
+ const POLL_TIMEOUT_MS = 10_000
26
+ const POLL_INTERVAL_MS = 500
27
+
28
+ const find_log_for_url = async (sdk: Session, url: string) => {
29
+ const start = Date.now()
30
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
31
+ const logs = await sdk.api.webhook_logs.getSome({ limit: 50, sort: 'newFirst' as any }) as Array<any>
32
+ const match = logs.find(l => l.url === url)
33
+ if (match) return match
34
+ await wait(undefined, POLL_INTERVAL_MS)
35
+ }
36
+ return null
37
+ }
38
+
39
+ const get_failure_count = async (sdk: Session) => {
40
+ const configs = await sdk.api.webhooks.getSome() as Array<any>
41
+ return (configs[0]?.deliveryFailureCount ?? 0) as number
42
+ }
43
+
44
+ // the $inc on the webhook config is fire-and-forget — poll until it lands
45
+ const wait_for_failure_count = async (sdk: Session, expected: number) => {
46
+ const start = Date.now()
47
+ let count = -1
48
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
49
+ count = await get_failure_count(sdk)
50
+ if (count === expected) return count
51
+ await wait(undefined, POLL_INTERVAL_MS)
52
+ }
53
+ return count
54
+ }
55
+
56
+ // Main test function that can be called independently
57
+ export const webhook_error_handling_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
58
+ log_header("Webhook Error Handling Tests")
59
+
60
+ // local receiver: error paths for failure scenarios, a healthy path that verifies integrity
61
+ const app = express()
62
+ app.use(bodyParser.json({ limit: '25mb' }))
63
+
64
+ let successIntegrityVerified = false
65
+ app.post('/fail-400', (_, res) => { res.status(400).json({ error: 'bad request' }) })
66
+ app.post('/fail-500', (_, res) => { res.status(500).json({ error: 'server error' }) })
67
+ app.post('/success', (req, res) => {
68
+ const { records, timestamp, integrity } = req.body
69
+ successIntegrityVerified = (
70
+ sha256((records ?? []).map((r: { id: string }) => r.id).join('') + timestamp + TEST_SECRET) === integrity
71
+ )
72
+ res.status(200).json({})
73
+ })
74
+
75
+ const server: http.Server = await new Promise(resolve => {
76
+ const s = app.listen(0, '127.0.0.1', () => resolve(s))
77
+ })
78
+ const receiverBase = `http://127.0.0.1:${(server.address() as any).port}`
79
+
80
+ const enduserIds: string[] = []
81
+ const trigger_delivery = async (label: string) => {
82
+ const enduser = await sdk.api.endusers.createOne({ email: `webhook-error-${label}-${Date.now()}@tellescope.com` })
83
+ enduserIds.push(enduser.id)
84
+ }
85
+
86
+ try {
87
+ await async_test(
88
+ 'configure webhook',
89
+ () => sdk.api.webhooks.configure({ url: ENOTFOUND_URL, secret: TEST_SECRET }),
90
+ { shouldError: false, onResult: () => true },
91
+ )
92
+ await sdk.api.webhooks.update({ subscriptionUpdates: { endusers: { create: true, update: false, delete: false } } })
93
+
94
+ const initialCount = await get_failure_count(sdk)
95
+
96
+ // Scenario 1: ENOTFOUND — no HTTP response at all
97
+ await trigger_delivery('enotfound')
98
+ const enotfoundLog = await find_log_for_url(sdk, ENOTFOUND_URL)
99
+ assert(!!enotfoundLog, 'no webhook_logs entry for ENOTFOUND delivery', 'ENOTFOUND failure logged to webhook_logs')
100
+ assert(
101
+ typeof enotfoundLog?.response === 'string' && enotfoundLog.response.length > 0,
102
+ `expected string error message response, got: ${JSON.stringify(enotfoundLog?.response)}`,
103
+ 'ENOTFOUND log has error message as response'
104
+ )
105
+ assert(enotfoundLog?.responseCode === undefined, 'unexpected responseCode for ENOTFOUND', 'ENOTFOUND log has no responseCode')
106
+ assert(
107
+ await wait_for_failure_count(sdk, initialCount + 1) === initialCount + 1,
108
+ 'deliveryFailureCount not incremented after ENOTFOUND',
109
+ 'deliveryFailureCount incremented after ENOTFOUND'
110
+ )
111
+
112
+ // Scenario 2a: HTTP 400 from the receiver
113
+ await sdk.api.webhooks.update({ url: `${receiverBase}/fail-400` })
114
+ await trigger_delivery('http400')
115
+ const log400 = await find_log_for_url(sdk, `${receiverBase}/fail-400`)
116
+ assert(log400?.responseCode === 400, `expected responseCode 400, got ${log400?.responseCode}`, 'HTTP 400 responseCode recorded')
117
+ assert(
118
+ await wait_for_failure_count(sdk, initialCount + 2) === initialCount + 2,
119
+ 'deliveryFailureCount not incremented after HTTP 400',
120
+ 'deliveryFailureCount incremented after HTTP 400'
121
+ )
122
+
123
+ // Scenario 2b: HTTP 500 from the receiver
124
+ await sdk.api.webhooks.update({ url: `${receiverBase}/fail-500` })
125
+ await trigger_delivery('http500')
126
+ const log500 = await find_log_for_url(sdk, `${receiverBase}/fail-500`)
127
+ assert(log500?.responseCode === 500, `expected responseCode 500, got ${log500?.responseCode}`, 'HTTP 500 responseCode recorded')
128
+ assert(
129
+ await wait_for_failure_count(sdk, initialCount + 3) === initialCount + 3,
130
+ 'deliveryFailureCount not incremented after HTTP 500',
131
+ 'deliveryFailureCount incremented after HTTP 500'
132
+ )
133
+
134
+ // Scenario 3 (timeout): skipped — WEBHOOK_TIMEOUT_MS is a constant (15s), not env-overridable,
135
+ // and a 15s+ non-responding receiver would make this suite unacceptably slow.
136
+ // Covered by security/F-0155-webhook-timeout.test.ts via send_automation_webhook.
137
+
138
+ // Scenario 4: success path unchanged — 200 logged, counter untouched
139
+ await sdk.api.webhooks.update({ url: `${receiverBase}/success` })
140
+ await trigger_delivery('success')
141
+ const successLog = await find_log_for_url(sdk, `${receiverBase}/success`)
142
+ assert(successLog?.responseCode === 200, `expected responseCode 200, got ${successLog?.responseCode}`, 'success responseCode recorded')
143
+ assert(successIntegrityVerified, 'integrity check failed on success delivery', 'success delivery integrity verified')
144
+ await wait(undefined, 1000) // allow any (erroneous) counter update to land before checking
145
+ assert(
146
+ await get_failure_count(sdk) === initialCount + 3,
147
+ 'deliveryFailureCount changed on successful delivery',
148
+ 'deliveryFailureCount untouched on success'
149
+ )
150
+
151
+ // deliveryFailureCount is server-managed: not settable through the public update endpoint
152
+ const configs = await sdk.api.webhooks.getSome() as Array<any>
153
+ const webhookId = configs[0]?.id
154
+ await sdk.api.webhooks.updateOne(webhookId, { deliveryFailureCount: 9999 } as any).catch(() => {}) // may reject (readonly)
155
+ assert(
156
+ await get_failure_count(sdk) === initialCount + 3,
157
+ 'customer was able to overwrite deliveryFailureCount',
158
+ 'deliveryFailureCount not customer-settable'
159
+ )
160
+ } finally {
161
+ // stop deliveries for subsequent tests and shut down the local receiver
162
+ await sdk.api.webhooks.update({ subscriptionUpdates: { endusers: { create: false, update: false, delete: false } } }).catch(() => {})
163
+ server.close()
164
+ for (const id of enduserIds) {
165
+ try { await sdk.api.endusers.deleteOne(id) } catch {}
166
+ }
167
+ }
168
+ }
169
+
170
+ // Allow running this test file independently
171
+ if (require.main === module) {
172
+ console.log(`🌐 Using API URL: ${host}`)
173
+ const sdk = new Session({ host })
174
+ const sdkNonAdmin = new Session({ host })
175
+
176
+ const runTests = async () => {
177
+ await setup_tests(sdk, sdkNonAdmin)
178
+ await webhook_error_handling_tests({ sdk, sdkNonAdmin })
179
+ }
180
+
181
+ runTests()
182
+ .then(() => {
183
+ console.log("✅ Webhook error handling test suite completed successfully")
184
+ process.exit(0)
185
+ })
186
+ .catch((error) => {
187
+ console.error("❌ Webhook error handling test suite failed:", error)
188
+ process.exit(1)
189
+ })
190
+ }
@@ -136,6 +136,7 @@ import { date_string_validation_tests } from "./api_tests/date_string_validation
136
136
  import { enduser_session_invalidation_tests } from "./api_tests/enduser_session_invalidation.test";
137
137
  import { enduser_cross_access_isolation_tests } from "./api_tests/enduser_cross_access_isolation.test";
138
138
  import { calendar_event_webhook_template_tests } from "./api_tests/calendar_event_webhook_template.test";
139
+ import { webhook_error_handling_tests } from "./api_tests/webhook_error_handling.test";
139
140
 
140
141
  const UniquenessViolationMessage = 'Uniqueness Violation'
141
142
 
@@ -15038,6 +15039,7 @@ const ip_address_form_tests = async () => {
15038
15039
  await replace_form_field_template_values_tests()
15039
15040
  await mfa_tests()
15040
15041
  await setup_tests(sdk, sdkNonAdmin)
15042
+ await custom_dashboards_tests({ sdk, sdkNonAdmin })
15041
15043
  await register_business_id_injection_tests({ sdk, sdkNonAdmin })
15042
15044
  await automation_trigger_tests()
15043
15045
  await resource_access_tags_tests({ sdk, sdkNonAdmin })
@@ -15053,6 +15055,7 @@ const ip_address_form_tests = async () => {
15053
15055
  await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
15054
15056
  await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
15055
15057
  await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
15058
+ await webhook_error_handling_tests({ sdk, sdkNonAdmin })
15056
15059
  await outbound_chat_sent_trigger_tests({ sdk })
15057
15060
  await enduser_login_rate_limits_tests({ sdk, sdkNonAdmin })
15058
15061
  await data_sync_redaction_bypass_tests({ sdk, sdkNonAdmin })
@@ -15091,7 +15094,6 @@ const ip_address_form_tests = async () => {
15091
15094
  await calendar_event_limits_tests({ sdk, sdkNonAdmin })
15092
15095
  await get_some_projection_tests({ sdk, sdkNonAdmin })
15093
15096
  await elation_user_id_tests({ sdk, sdkNonAdmin })
15094
- await custom_dashboards_tests({ sdk, sdkNonAdmin })
15095
15097
  await concurrent_build_threads_tests({ sdk, sdkNonAdmin })
15096
15098
  await custom_aggregation_tests({ sdk, sdkNonAdmin })
15097
15099
  await no_access_permission_checks_tests({ sdk, sdkNonAdmin })
Binary file