@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.
- package/lib/cjs/enduser.d.ts +1 -0
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/sdk.d.ts +1 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/session.d.ts +1 -0
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js +258 -32
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.js +183 -0
- package/lib/cjs/tests/api_tests/gmail_sync_related_contact_emails.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.js +324 -0
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +172 -168
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +2 -2
- package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/custom_dashboards.test.js +259 -33
- package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.js +179 -0
- package/lib/esm/tests/api_tests/gmail_sync_related_contact_emails.test.js.map +1 -0
- package/lib/esm/tests/api_tests/webhook_error_handling.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/webhook_error_handling.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/webhook_error_handling.test.js +317 -0
- package/lib/esm/tests/api_tests/webhook_error_handling.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +172 -168
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/tests/api_tests/custom_dashboards.test.ts +208 -0
- package/src/tests/api_tests/webhook_error_handling.test.ts +190 -0
- package/src/tests/tests.ts +3 -1
- 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
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -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 })
|
package/test_generated.pdf
CHANGED
|
Binary file
|