@tellescope/sdk 0.0.17 → 0.0.21
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 +32 -3
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/enduser.js +6 -3
- package/lib/cjs/enduser.js.map +1 -1
- package/lib/cjs/sdk.d.ts +46 -6
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +10 -6
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/session.d.ts +4 -21
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/session.js +1 -0
- package/lib/cjs/session.js.map +1 -1
- package/lib/cjs/tests/public_endpoint_tests.js +1 -1
- package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
- package/lib/cjs/tests/socket_tests.d.ts +1 -1
- package/lib/cjs/tests/socket_tests.d.ts.map +1 -1
- package/lib/cjs/tests/socket_tests.js +124 -7
- package/lib/cjs/tests/socket_tests.js.map +1 -1
- package/lib/cjs/tests/tests.js +156 -113
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/webhooks_tests.d.ts +2 -0
- package/lib/cjs/tests/webhooks_tests.d.ts.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +261 -0
- package/lib/cjs/tests/webhooks_tests.js.map +1 -0
- package/lib/esm/enduser.d.ts +32 -3
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/enduser.js +6 -3
- package/lib/esm/enduser.js.map +1 -1
- package/lib/esm/sdk.d.ts +46 -6
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +10 -6
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/session.d.ts +4 -21
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/session.js +1 -0
- package/lib/esm/session.js.map +1 -1
- package/lib/esm/tests/public_endpoint_tests.js +1 -1
- package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
- package/lib/esm/tests/socket_tests.d.ts +1 -1
- package/lib/esm/tests/socket_tests.d.ts.map +1 -1
- package/lib/esm/tests/socket_tests.js +122 -7
- package/lib/esm/tests/socket_tests.js.map +1 -1
- package/lib/esm/tests/tests.js +156 -113
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/webhooks_tests.d.ts +2 -0
- package/lib/esm/tests/webhooks_tests.d.ts.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +256 -0
- package/lib/esm/tests/webhooks_tests.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -11
- package/src/enduser.ts +12 -7
- package/src/sdk.ts +32 -13
- package/src/session.ts +3 -23
- package/src/tests/public_endpoint_tests.ts +1 -1
- package/src/tests/socket_tests.ts +67 -4
- package/src/tests/tests.ts +60 -37
- package/src/tests/webhooks_tests.ts +183 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
1
2
|
import {
|
|
2
3
|
assert,
|
|
3
4
|
async_test,
|
|
@@ -17,6 +18,7 @@ import { EnduserSession } from "../enduser"
|
|
|
17
18
|
import { Session, /* APIQuery */ } from "../sdk"
|
|
18
19
|
import { PLACEHOLDER_ID } from "@tellescope/constants"
|
|
19
20
|
|
|
21
|
+
export const get_sha256 = (s='') => createHash('sha256').update(s).digest('hex')
|
|
20
22
|
|
|
21
23
|
const host = process.env.TEST_URL || 'http://localhost:8080'
|
|
22
24
|
const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
|
|
@@ -198,28 +200,89 @@ const enduser_tests = async () => {
|
|
|
198
200
|
// await user1.api.chats.deleteOne(messageToUser.id)
|
|
199
201
|
}
|
|
200
202
|
|
|
203
|
+
const TEST_SESSION_DURATION = 2 // seconds
|
|
204
|
+
const SESSION_TIMEOUT_DELAY = 2000 // ms
|
|
205
|
+
const deauthentication_tests = async (byTimeout=false) => {
|
|
206
|
+
log_header(`Unauthenticated Tests ${byTimeout ? '- With Timeout, requires Worker' : '- With Manual Logout' }`)
|
|
207
|
+
|
|
208
|
+
const enduser = await user1.api.endusers.createOne({ email: "socketenduser@tellescope.com" })
|
|
209
|
+
await user1.api.endusers.set_password({ id: enduser.id, password: 'enduserPassword!' })
|
|
210
|
+
await enduserSDK.authenticate(enduser.email as string, 'enduserPassword!', { expirationInSeconds: byTimeout ? TEST_SESSION_DURATION : undefined })
|
|
211
|
+
await wait(undefined, 25)
|
|
212
|
+
|
|
213
|
+
const room = await user1.api.chat_rooms.createOne({
|
|
214
|
+
type: 'external',
|
|
215
|
+
userIds: [user1.userInfo.id],
|
|
216
|
+
enduserIds: [enduser.id],
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const userEvents = [] as ChatMessage[]
|
|
220
|
+
const enduserEvents = [] as ChatMessage[]
|
|
221
|
+
user1.subscribe({ [room.id]: 'chats' })
|
|
222
|
+
enduserSDK.subscribe({ [room.id]: 'chats' })
|
|
223
|
+
user1.handle_events({ 'created-chats': rs => userEvents.push(...rs) })
|
|
224
|
+
enduserSDK.handle_events({ 'created-chats': rs => enduserEvents.push(...rs) })
|
|
225
|
+
await wait(undefined, 10)
|
|
226
|
+
|
|
227
|
+
if (!byTimeout) {
|
|
228
|
+
await enduserSDK.api.endusers.logout()
|
|
229
|
+
} else {
|
|
230
|
+
await wait(undefined, TEST_SESSION_DURATION * 1000 + SESSION_TIMEOUT_DELAY)
|
|
231
|
+
}
|
|
232
|
+
await user1.api.chats.createOne({ roomId: room.id, message: "Hello!" })
|
|
233
|
+
await wait(undefined, 25)
|
|
234
|
+
assert(objects_equivalent(enduserEvents[0], undefined), 'enduser got message after logout on socket', 'enduser logged out')
|
|
235
|
+
|
|
236
|
+
// re-authenticate enduser to send message to user
|
|
237
|
+
await enduserSDK.authenticate(enduser.email as string, 'enduserPassword!')
|
|
238
|
+
|
|
239
|
+
// ensure user logged out appropriately for not receiving message
|
|
240
|
+
await user1.logout()
|
|
241
|
+
if (byTimeout) {
|
|
242
|
+
await user1.authenticate(email, password, { expirationInSeconds: byTimeout ? TEST_SESSION_DURATION : undefined } )
|
|
243
|
+
await wait(undefined, TEST_SESSION_DURATION * 1000 + SESSION_TIMEOUT_DELAY)
|
|
244
|
+
}
|
|
245
|
+
await enduserSDK.api.chats.createOne({ roomId: room.id, message: "Hello right back!" })
|
|
246
|
+
await wait(undefined, 25)
|
|
247
|
+
assert(objects_equivalent(userEvents[0], undefined), 'user got message after logout', 'user logged out')
|
|
248
|
+
|
|
249
|
+
// must come before cleanup, so cleanup works
|
|
250
|
+
await user1.authenticate(email, password), // reauthenticate for later tests as needed
|
|
251
|
+
|
|
252
|
+
// cleanup
|
|
253
|
+
await Promise.all([
|
|
254
|
+
user1.api.endusers.deleteOne(enduser.id),
|
|
255
|
+
user1.api.chat_rooms.deleteOne(room.id), // deletes chats as side effect
|
|
256
|
+
])
|
|
257
|
+
}
|
|
258
|
+
|
|
201
259
|
(async () => {
|
|
202
260
|
log_header("Sockets")
|
|
203
261
|
|
|
204
262
|
try {
|
|
205
|
-
await user1.authenticate(email, password
|
|
263
|
+
await user1.authenticate(email, password)
|
|
206
264
|
await user1.reset_db()
|
|
207
|
-
await user2.authenticate(email2, password2
|
|
265
|
+
await user2.authenticate(email2, password2) // generate authToken + socket connection for API keyj
|
|
208
266
|
await wait(undefined, 25)
|
|
209
267
|
|
|
210
268
|
let loopCount = 0
|
|
211
269
|
while (!(user1.socketAuthenticated && user2.socketAuthenticated) && ++loopCount < 10) {
|
|
212
|
-
|
|
270
|
+
if (!user1.socketAuthenticated) user1.authenticate_socket()
|
|
271
|
+
if (!user2.socketAuthenticated) user2.authenticate_socket()
|
|
213
272
|
await wait(undefined, 100)
|
|
214
273
|
}
|
|
215
274
|
if (loopCount === 10) {
|
|
216
275
|
console.log("Failed to authenticate")
|
|
217
276
|
process.exit()
|
|
218
277
|
}
|
|
219
|
-
|
|
278
|
+
|
|
220
279
|
await enduser_tests()
|
|
221
280
|
await basic_tests()
|
|
222
281
|
await access_tests()
|
|
282
|
+
|
|
283
|
+
await deauthentication_tests() // should come last!
|
|
284
|
+
await deauthentication_tests(true) // should come last!
|
|
285
|
+
|
|
223
286
|
} catch(err) {
|
|
224
287
|
console.error(err)
|
|
225
288
|
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -71,7 +71,7 @@ const userId = '60398b0231a295e64f084fd9'
|
|
|
71
71
|
// await sdk.api.endusers.deleteOne(enduser.id)
|
|
72
72
|
// }
|
|
73
73
|
|
|
74
|
-
const sdk = new Session()
|
|
74
|
+
const sdk = new Session({ host })
|
|
75
75
|
const sdkOther = new Session({ host, apiKey: "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7" })
|
|
76
76
|
const enduserSDK = new EnduserSession({ host })
|
|
77
77
|
// const sdkOtherEmail = "sebass@tellescope.com"
|
|
@@ -90,8 +90,6 @@ const setup_tests = async () => {
|
|
|
90
90
|
await async_test('test_online', sdk.test_online, { expectedResult: 'API V1 Online' })
|
|
91
91
|
await async_test('test_authenticated', sdk.test_authenticated, { expectedResult: 'Authenticated!' })
|
|
92
92
|
|
|
93
|
-
// console.log(await sdk.api.api_keys.createOne({}))
|
|
94
|
-
|
|
95
93
|
await async_test(
|
|
96
94
|
'test_authenticated (with API Key)',
|
|
97
95
|
(new Session({ host, apiKey: '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' /* local test key */ })).test_authenticated,
|
|
@@ -100,7 +98,7 @@ const setup_tests = async () => {
|
|
|
100
98
|
|
|
101
99
|
await sdk.logout()
|
|
102
100
|
await async_test<string, string>('test_authenticated - (logout invalidates jwt)', sdk.test_authenticated, { shouldError: true, onError: e => e === 'Unauthenticated' })
|
|
103
|
-
await sdk.authenticate(email, password
|
|
101
|
+
await sdk.authenticate(email, password)
|
|
104
102
|
await async_test('test_authenticated (re-authenticated)', sdk.test_authenticated, { expectedResult: 'Authenticated!' })
|
|
105
103
|
|
|
106
104
|
await async_test('reset_db', () => sdk.reset_db(), passOnVoid)
|
|
@@ -191,6 +189,20 @@ const threadKeyTests = async () => {
|
|
|
191
189
|
])
|
|
192
190
|
}
|
|
193
191
|
|
|
192
|
+
const badInputTests = async () => {
|
|
193
|
+
log_header("Bad Input Tests")
|
|
194
|
+
await async_test(
|
|
195
|
+
`_-prefixed fields are not allowed`,
|
|
196
|
+
() => sdk.api.endusers.createOne({ email: 'failure@tellescope.com', fields: { "_notallowed": 'hello' } }),
|
|
197
|
+
{ shouldError: true, onError: e => e.message === "Error parsing field fields: Fields that start with '_' are not allowed" },
|
|
198
|
+
)
|
|
199
|
+
await async_test(
|
|
200
|
+
`_-prefixed fields are not allowed`,
|
|
201
|
+
() => sdk.api.notes.createOne({ enduserId: PLACEHOLDER_ID, fields: { "_notallowed": 'hello' } }),
|
|
202
|
+
{ shouldError: true, onError: e => e.message === "Error parsing field fields: Fields that start with '_' are not allowed" },
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
194
206
|
const filterTests = async () => {
|
|
195
207
|
const enduser = await sdk.api.endusers.createOne({ email: 'filtertests@tellescope.com', fname: 'test', fields: { field1: 'value1', field2: 'value2' } })
|
|
196
208
|
const otherEnduser = await sdk.api.endusers.createOne({ email: 'other@tellescope.com' })
|
|
@@ -199,6 +211,17 @@ const filterTests = async () => {
|
|
|
199
211
|
() => sdk.api.endusers.getSome({ filter: { email: 'filtertests@tellescope.com' }}),
|
|
200
212
|
{ onResult: es => es.length === 1 && es[0].id === enduser.id },
|
|
201
213
|
)
|
|
214
|
+
await async_test(
|
|
215
|
+
`find enduser by _exists filter`,
|
|
216
|
+
() => sdk.api.endusers.getSome({ filter: { fname: { _exists: true } }}),
|
|
217
|
+
{ onResult: es => es.length === 1 && es[0].id === enduser.id },
|
|
218
|
+
)
|
|
219
|
+
await async_test(
|
|
220
|
+
`find enduser by _exists filter (findOne)`,
|
|
221
|
+
// () => sdk.api.endusers.getOne({ fname: { _exists: true } }),
|
|
222
|
+
() => sdk.api.endusers.getOne({ fname: { _exists: true } }),
|
|
223
|
+
{ onResult: e => e.id === enduser.id },
|
|
224
|
+
)
|
|
202
225
|
await async_test(
|
|
203
226
|
`find enduser by compound filter`,
|
|
204
227
|
() => sdk.api.endusers.getSome({ filter: { email: 'filtertests@tellescope.com', fname: 'test' }}),
|
|
@@ -265,13 +288,21 @@ const updatesTests = async () => {
|
|
|
265
288
|
|
|
266
289
|
const generateEnduserAuthTests = async () => {
|
|
267
290
|
log_header("Generated Enduser authToken Tests")
|
|
268
|
-
const
|
|
291
|
+
const externalId = '1029f9v9sjd0as'
|
|
292
|
+
const e = await sdk.api.endusers.createOne({ email: 'generated@tellescope.com', phone: '+15555555555', externalId })
|
|
293
|
+
|
|
269
294
|
const { authToken, enduser } = await sdk.api.endusers.generate_auth_token({ id: e.id })
|
|
270
295
|
assert(!!authToken && !!enduser, 'invalid returned values', 'Generate authToken and get enduser')
|
|
271
|
-
|
|
272
296
|
const { isAuthenticated } = await sdk.api.endusers.is_authenticated({ id: enduser.id, authToken })
|
|
273
297
|
assert(isAuthenticated, 'invalid authToken generated for enduser', 'Generate authToken for enduser is valid')
|
|
274
298
|
|
|
299
|
+
const { authToken: authTokenUID, enduser: enduser2 } = await sdk.api.endusers.generate_auth_token({ externalId })
|
|
300
|
+
assert(!!authTokenUID && !!enduser2, 'invalid returned values eid', 'Generate authToken and get enduser eid')
|
|
301
|
+
assert((
|
|
302
|
+
await sdk.api.endusers.is_authenticated({ id: enduser2.id, authToken: authTokenUID })).isAuthenticated,
|
|
303
|
+
'invalid authToken generated for enduser', 'Generate authToken for enduser is valid'
|
|
304
|
+
)
|
|
305
|
+
|
|
275
306
|
await async_test(
|
|
276
307
|
`auth by externalId`, () => sdk.api.endusers.generate_auth_token({ externalId: e.externalId }), passOnVoid,
|
|
277
308
|
)
|
|
@@ -419,7 +450,7 @@ const run_generated_tests = async <N extends ModelName>({ queries, model, name,
|
|
|
419
450
|
}
|
|
420
451
|
await async_test(
|
|
421
452
|
`get-${singularName}`,
|
|
422
|
-
() => queries.getOne(_id
|
|
453
|
+
() => queries.getOne(_id),
|
|
423
454
|
{ onResult: d => {
|
|
424
455
|
if (!d?.id) return false
|
|
425
456
|
|
|
@@ -452,7 +483,7 @@ const run_generated_tests = async <N extends ModelName>({ queries, model, name,
|
|
|
452
483
|
)
|
|
453
484
|
await async_test(
|
|
454
485
|
`get-${singularName} (verify delete)`,
|
|
455
|
-
() => queries.getOne(_id
|
|
486
|
+
() => queries.getOne(_id),
|
|
456
487
|
{ shouldError: true, onError: e => e.message === 'Could not find a record for the given id' }
|
|
457
488
|
)
|
|
458
489
|
}
|
|
@@ -840,32 +871,22 @@ const chat_tests = async() => {
|
|
|
840
871
|
const chat = await sdk.api.chats.createOne({ roomId: room.id, message: "Hello!" })
|
|
841
872
|
const chat2 = await sdk.api.chats.createOne({ roomId: room.id, message: "Hello..." })
|
|
842
873
|
|
|
843
|
-
await async_test(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
)
|
|
874
|
+
// await async_test(
|
|
875
|
+
// `get-chat (without filter)`,
|
|
876
|
+
// () => sdk.api.chats.getOne(chat.id),
|
|
877
|
+
// { shouldError: true, onError: () => true }
|
|
878
|
+
// )
|
|
848
879
|
await async_test(
|
|
849
880
|
`get-chats (without filter)`,
|
|
850
881
|
() => sdk.api.chats.getSome({}),
|
|
851
882
|
{ shouldError: true, onError: () => true }
|
|
852
883
|
)
|
|
853
|
-
await async_test(
|
|
854
|
-
`get-chat (with filter)`,
|
|
855
|
-
() => sdk.api.chats.getOne(chat.id, { roomId: room.id }),
|
|
856
|
-
{ onResult: c => c?.id === chat.id }
|
|
857
|
-
)
|
|
858
884
|
await async_test(
|
|
859
885
|
`get-chats (with filter)`,
|
|
860
886
|
() => sdk.api.chats.getSome({ filter: { roomId: room.id } }),
|
|
861
887
|
{ onResult: c => c?.length === 2 }
|
|
862
888
|
)
|
|
863
889
|
|
|
864
|
-
await async_test(
|
|
865
|
-
`get-chat not allowed`,
|
|
866
|
-
() => sdk2.api.chats.getOne(chat.id, { roomId: room.id }),
|
|
867
|
-
{ shouldError: true, onError: e => e.message === 'You do not have permission to access this resource' }
|
|
868
|
-
)
|
|
869
890
|
await async_test(
|
|
870
891
|
`get-chats not allowed`,
|
|
871
892
|
() => sdk2.api.chats.getSome({ filter: { roomId: room.id } }),
|
|
@@ -894,12 +915,12 @@ const chat_tests = async() => {
|
|
|
894
915
|
await wait(undefined, 25)
|
|
895
916
|
await async_test(
|
|
896
917
|
`get-chat (deleted as dependency of room 1)`,
|
|
897
|
-
() => sdk.api.chats.getOne(chat.id
|
|
918
|
+
() => sdk.api.chats.getOne(chat.id),
|
|
898
919
|
{ shouldError: true, onError: e => e.message === 'Could not find a record for the given id' }
|
|
899
920
|
)
|
|
900
921
|
await async_test(
|
|
901
922
|
`get-chat (deleted as dependency of room 2)`,
|
|
902
|
-
() => sdk.api.chats.getOne(chat2.id
|
|
923
|
+
() => sdk.api.chats.getOne(chat2.id),
|
|
903
924
|
{ shouldError: true, onError: e => e.message === 'Could not find a record for the given id' }
|
|
904
925
|
)
|
|
905
926
|
|
|
@@ -908,12 +929,12 @@ const chat_tests = async() => {
|
|
|
908
929
|
const sharedChat2 = await sdk2.api.chats.createOne({ roomId: sharedRoom.id, message: "Hello there!", })
|
|
909
930
|
await async_test(
|
|
910
931
|
`get-chat (shared, user1)`,
|
|
911
|
-
() => sdk.api.chats.getOne(sharedChat.id
|
|
932
|
+
() => sdk.api.chats.getOne(sharedChat.id),
|
|
912
933
|
{ onResult: r => r.id === sharedChat.id }
|
|
913
934
|
)
|
|
914
935
|
await async_test(
|
|
915
936
|
`get-chat (shared, user2)`,
|
|
916
|
-
() => sdk2.api.chats.getOne(sharedChat.id
|
|
937
|
+
() => sdk2.api.chats.getOne(sharedChat.id),
|
|
917
938
|
{ onResult: r => r.id === sharedChat.id }
|
|
918
939
|
)
|
|
919
940
|
await async_test(
|
|
@@ -936,7 +957,7 @@ const chat_tests = async() => {
|
|
|
936
957
|
await sdk.api.chats.deleteOne(chatNull.id)
|
|
937
958
|
await async_test(
|
|
938
959
|
`get-chat (setNull working)`,
|
|
939
|
-
() => sdk.api.chats.getOne(chat2Null.id
|
|
960
|
+
() => sdk.api.chats.getOne(chat2Null.id),
|
|
940
961
|
{ onResult: c => c.replyId === null }
|
|
941
962
|
)
|
|
942
963
|
}
|
|
@@ -952,47 +973,48 @@ const enduserAccessTests = async () => {
|
|
|
952
973
|
for (const n in schema) {
|
|
953
974
|
const endpoint = url_safe_path(n)
|
|
954
975
|
const model = schema[n as keyof typeof schema]
|
|
976
|
+
if (n === 'webhooks') continue // no default endpoints implemented
|
|
955
977
|
|
|
956
978
|
if (!model?.enduserActions?.read && (model.defaultActions.read || model.customActions.read)) {
|
|
957
979
|
await async_test(
|
|
958
980
|
`no-enduser-access getOne (${endpoint})`,
|
|
959
981
|
() => enduserSDK.GET(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
960
|
-
{ shouldError: true, onError: (e:
|
|
982
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
961
983
|
)
|
|
962
984
|
}
|
|
963
985
|
if (!model.enduserActions?.readMany && (model.defaultActions.readMany || model.customActions.readMany)) {
|
|
964
986
|
await async_test(
|
|
965
987
|
`no-enduser-access getSome (${endpoint})`,
|
|
966
988
|
() => enduserSDK.GET(`/v1/${endpoint}`),
|
|
967
|
-
{ shouldError: true, onError: (e:
|
|
989
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
968
990
|
)
|
|
969
991
|
}
|
|
970
992
|
if (!model.enduserActions?.create && (model.defaultActions.create || model.customActions.create)) {
|
|
971
993
|
await async_test(
|
|
972
994
|
`no-enduser-access createOne (${endpoint})`,
|
|
973
995
|
() => enduserSDK.POST(`/v1/${endpoint.substring(0, endpoint.length - 1)}`),
|
|
974
|
-
{ shouldError: true, onError: (e:
|
|
996
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
975
997
|
)
|
|
976
998
|
}
|
|
977
999
|
if (!model.enduserActions?.createMany && (model.defaultActions.createMany || model.customActions.createMany)) {
|
|
978
1000
|
await async_test(
|
|
979
1001
|
`no-enduser-access createMany (${endpoint})`,
|
|
980
1002
|
() => enduserSDK.POST(`/v1/${endpoint}`),
|
|
981
|
-
{ shouldError: true, onError: (e:
|
|
1003
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
982
1004
|
)
|
|
983
1005
|
}
|
|
984
1006
|
if (!model.enduserActions?.update && (model.defaultActions.update || model.customActions.update)) {
|
|
985
1007
|
await async_test(
|
|
986
1008
|
`no-enduser-access update (${endpoint})`,
|
|
987
1009
|
() => enduserSDK.PATCH(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
988
|
-
{ shouldError: true, onError: (e:
|
|
1010
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
989
1011
|
)
|
|
990
1012
|
}
|
|
991
1013
|
if (!model.enduserActions?.delete && (model.defaultActions.delete || model.customActions.delete)) {
|
|
992
1014
|
await async_test(
|
|
993
1015
|
`no-enduser-access delete (${endpoint})`,
|
|
994
1016
|
() => enduserSDK.DELETE(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
995
|
-
{ shouldError: true, onError: (e:
|
|
1017
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
996
1018
|
)
|
|
997
1019
|
}
|
|
998
1020
|
}
|
|
@@ -1055,16 +1077,17 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
|
|
|
1055
1077
|
tickets: () => {},
|
|
1056
1078
|
meetings: () => {},
|
|
1057
1079
|
notes: () => {},
|
|
1080
|
+
webhooks: () => {},
|
|
1058
1081
|
};
|
|
1059
1082
|
|
|
1060
1083
|
(async () => {
|
|
1061
|
-
await sdk.authenticate(email, password, host)
|
|
1062
|
-
|
|
1063
1084
|
log_header("API")
|
|
1064
|
-
|
|
1085
|
+
|
|
1065
1086
|
try {
|
|
1087
|
+
await sdk.authenticate(email, password)
|
|
1066
1088
|
await setup_tests()
|
|
1067
1089
|
await multi_tenant_tests() // should come right after setup tests
|
|
1090
|
+
await badInputTests()
|
|
1068
1091
|
await filterTests()
|
|
1069
1092
|
await updatesTests()
|
|
1070
1093
|
await threadKeyTests()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import bodyParser from 'body-parser'
|
|
3
|
+
import crypto from "crypto"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assert,
|
|
7
|
+
async_test,
|
|
8
|
+
log_header,
|
|
9
|
+
wait,
|
|
10
|
+
} from "@tellescope/testing"
|
|
11
|
+
import {
|
|
12
|
+
objects_equivalent
|
|
13
|
+
} from "@tellescope/utilities"
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
WEBHOOK_MODELS,
|
|
17
|
+
WebhookSupportedModel,
|
|
18
|
+
WebhookRecord,
|
|
19
|
+
WebhookCall,
|
|
20
|
+
CUDSubscription,
|
|
21
|
+
} from "@tellescope/types-models"
|
|
22
|
+
|
|
23
|
+
import { Session } from "../sdk"
|
|
24
|
+
|
|
25
|
+
const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
|
|
26
|
+
const [email2, password2] = [process.env.TEST_EMAIL_2, process.env.TEST_PASSWORD_2]
|
|
27
|
+
const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
|
|
28
|
+
if (!(email && password && email2 && password2 && nonAdminEmail && nonAdminPassword)) {
|
|
29
|
+
console.error("Set TEST_EMAIL and TEST_PASSWORD")
|
|
30
|
+
process.exit()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const sdk = new Session({ host: 'http://localhost:8080' })
|
|
35
|
+
const nonAdminSdk = new Session({ host: 'http://localhost:8080' })
|
|
36
|
+
|
|
37
|
+
const app = express()
|
|
38
|
+
app.use(bodyParser.urlencoded({ extended: true, limit: '25mb' }))
|
|
39
|
+
app.use(bodyParser.json({ limit: "25mb" }))
|
|
40
|
+
|
|
41
|
+
const PORT = 4000
|
|
42
|
+
const TEST_SECRET = "this is a test secret for verifying integrity of web hooks"
|
|
43
|
+
const webhookEndpoint = '/handle-webhook'
|
|
44
|
+
const webhookURL = `http://127.0.0.1:${PORT}${webhookEndpoint}`
|
|
45
|
+
|
|
46
|
+
const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex')
|
|
47
|
+
|
|
48
|
+
const verify_integrity = (records: WebhookRecord[], timestamp: string, integrity: string,) => (
|
|
49
|
+
sha256(records.map(r => r.id).join('') + timestamp + TEST_SECRET) === integrity
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const handledEvents: WebhookCall[] = []
|
|
53
|
+
app.post(webhookEndpoint, (req, res) => {
|
|
54
|
+
const body = req.body as WebhookCall
|
|
55
|
+
// console.log('got hook', body.records, body.timestamp, body.integrity)
|
|
56
|
+
|
|
57
|
+
if (!verify_integrity(body.records, body.timestamp, body.integrity)) {
|
|
58
|
+
console.error("Integrity check failed for request", JSON.stringify(body, null, 2))
|
|
59
|
+
process.exit()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handledEvents.push(req.body)
|
|
63
|
+
res.status(204).end()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const fullSubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
|
|
67
|
+
const emptySubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
|
|
68
|
+
for (const model in WEBHOOK_MODELS) {
|
|
69
|
+
fullSubscription[model as WebhookSupportedModel] = { create: true, update: true, delete: true }
|
|
70
|
+
emptySubscription[model as WebhookSupportedModel] = { create: false, update: false, delete: false }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let webhookIndex = 0
|
|
74
|
+
const check_next_webhook = async (evaluate: (hook: WebhookCall) => boolean, name: string, error: string, isSubscribed: boolean) => {
|
|
75
|
+
if (isSubscribed === false) return
|
|
76
|
+
|
|
77
|
+
await wait(undefined, 25) // wait for hook to post
|
|
78
|
+
|
|
79
|
+
const event = handledEvents[webhookIndex]
|
|
80
|
+
assert(!!event, 'did not get hook', 'got hook')
|
|
81
|
+
if (!event) return // ensure webhookIndex not incremented
|
|
82
|
+
|
|
83
|
+
const success = evaluate(event)
|
|
84
|
+
assert(success, error, name)
|
|
85
|
+
if (!success) { console.error('Got', event) }
|
|
86
|
+
|
|
87
|
+
webhookIndex++
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const chats_tests = async (isSubscribed: boolean) => {
|
|
91
|
+
log_header(`Chats Tests, isSubscribed=${isSubscribed}`)
|
|
92
|
+
const room = await sdk.api.chat_rooms.createOne({ userIds: [sdk.userInfo.id] })
|
|
93
|
+
|
|
94
|
+
const chat = await sdk.api.chats.createOne({ roomId: room.id, message: "Hello hello hi hello" })
|
|
95
|
+
await check_next_webhook(a => objects_equivalent(a.records, [chat]), 'Create chat error', 'Create chat webhook', isSubscribed)
|
|
96
|
+
|
|
97
|
+
// cleanup
|
|
98
|
+
await sdk.api.chat_rooms.deleteOne(room.id) // also cleans up messages
|
|
99
|
+
|
|
100
|
+
// when chatroom support added for webhooks, check deletion here
|
|
101
|
+
// await check_next_webhook(a => objects_equivalent(a.records, [chat_room]), 'Delete chat room error', 'Delete chat room webhook', isSubscribed)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const meetings_tests = async (isSubscribed: boolean) => {
|
|
105
|
+
log_header(`Meetings Tests, isSubscribed=${isSubscribed}`)
|
|
106
|
+
const meeting = await sdk.api.meetings.start_meeting()
|
|
107
|
+
|
|
108
|
+
await check_next_webhook(a => objects_equivalent(a.records, [meeting]), 'Create meeting error', 'Create meeting webhook', isSubscribed)
|
|
109
|
+
|
|
110
|
+
// cleanup
|
|
111
|
+
await sdk.api.meetings.end_meeting({ id: meeting.id }) // also cleans up messages
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const tests: { [K in WebhookSupportedModel]: (isSubscribed: boolean) => Promise<void> } = {
|
|
115
|
+
chats: chats_tests,
|
|
116
|
+
meetings: meetings_tests,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const run_tests = async () => {
|
|
120
|
+
log_header("Webhooks Tests")
|
|
121
|
+
await sdk.authenticate(email, password)
|
|
122
|
+
await sdk.reset_db()
|
|
123
|
+
await nonAdminSdk.authenticate(nonAdminEmail, nonAdminPassword)
|
|
124
|
+
|
|
125
|
+
await async_test(
|
|
126
|
+
'configure webhook is admin only',
|
|
127
|
+
() => nonAdminSdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
128
|
+
{ shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
|
|
129
|
+
)
|
|
130
|
+
await async_test(
|
|
131
|
+
'update webhook is admin only',
|
|
132
|
+
() => nonAdminSdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
|
|
133
|
+
{ shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await async_test(
|
|
137
|
+
'configure webhook',
|
|
138
|
+
() => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
139
|
+
{ onResult: _ => true }
|
|
140
|
+
)
|
|
141
|
+
await async_test(
|
|
142
|
+
'configure webhook (only callable once)',
|
|
143
|
+
() => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
144
|
+
{ shouldError: true, onError: e => e.message === "Only one webhook configuration is supported per organization. Use /update-webooks to update your configuration." }
|
|
145
|
+
)
|
|
146
|
+
await async_test(
|
|
147
|
+
'update webhook (set subscriptions)',
|
|
148
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
|
|
149
|
+
{ onResult: _ => true }
|
|
150
|
+
)
|
|
151
|
+
await async_test(
|
|
152
|
+
'update webhook invalid model',
|
|
153
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: { notAModel: { create: false } } as any }),
|
|
154
|
+
{ shouldError: true, onError: e => e.message === "Error parsing field subscriptionUpdates: Got unexpected field(s) [notAModel]" }
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
log_header("Webhooks Tests with Subscriptions")
|
|
158
|
+
for (const t in tests) {
|
|
159
|
+
await tests[t as keyof typeof tests](true)
|
|
160
|
+
}
|
|
161
|
+
const finalLength = handledEvents.length
|
|
162
|
+
|
|
163
|
+
await async_test(
|
|
164
|
+
'update webhook (set subscriptions empty)',
|
|
165
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: emptySubscription }),
|
|
166
|
+
{ onResult: _ => true }
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
log_header("Webhooks Tests without Subscriptions")
|
|
170
|
+
for (const t in tests) {
|
|
171
|
+
await tests[t as keyof typeof tests](false)
|
|
172
|
+
}
|
|
173
|
+
assert(finalLength === handledEvents.length, 'length changed after subscriptions', 'No webhooks posted when no subscription')
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
app.listen(PORT, async () => {
|
|
178
|
+
try {
|
|
179
|
+
await run_tests()
|
|
180
|
+
} catch(err) { console.error(err) }
|
|
181
|
+
|
|
182
|
+
process.exit()
|
|
183
|
+
})
|