@tellescope/sdk 0.0.19 → 0.0.23

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 (39) hide show
  1. package/lib/cjs/enduser.d.ts +166 -14
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/enduser.js +2 -0
  4. package/lib/cjs/enduser.js.map +1 -1
  5. package/lib/cjs/sdk.d.ts +174 -10
  6. package/lib/cjs/sdk.d.ts.map +1 -1
  7. package/lib/cjs/sdk.js +5 -0
  8. package/lib/cjs/sdk.js.map +1 -1
  9. package/lib/cjs/session.d.ts.map +1 -1
  10. package/lib/cjs/session.js.map +1 -1
  11. package/lib/cjs/tests/tests.js +93 -50
  12. package/lib/cjs/tests/tests.js.map +1 -1
  13. package/lib/cjs/tests/webhooks_tests.d.ts +2 -0
  14. package/lib/cjs/tests/webhooks_tests.d.ts.map +1 -0
  15. package/lib/cjs/tests/webhooks_tests.js +267 -0
  16. package/lib/cjs/tests/webhooks_tests.js.map +1 -0
  17. package/lib/esm/enduser.d.ts +166 -14
  18. package/lib/esm/enduser.d.ts.map +1 -1
  19. package/lib/esm/enduser.js +2 -0
  20. package/lib/esm/enduser.js.map +1 -1
  21. package/lib/esm/sdk.d.ts +174 -10
  22. package/lib/esm/sdk.d.ts.map +1 -1
  23. package/lib/esm/sdk.js +5 -0
  24. package/lib/esm/sdk.js.map +1 -1
  25. package/lib/esm/session.d.ts.map +1 -1
  26. package/lib/esm/session.js.map +1 -1
  27. package/lib/esm/tests/tests.js +93 -50
  28. package/lib/esm/tests/tests.js.map +1 -1
  29. package/lib/esm/tests/webhooks_tests.d.ts +2 -0
  30. package/lib/esm/tests/webhooks_tests.d.ts.map +1 -0
  31. package/lib/esm/tests/webhooks_tests.js +262 -0
  32. package/lib/esm/tests/webhooks_tests.js.map +1 -0
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +14 -11
  35. package/src/enduser.ts +8 -5
  36. package/src/sdk.ts +16 -4
  37. package/src/session.ts +0 -5
  38. package/src/tests/tests.ts +34 -9
  39. package/src/tests/webhooks_tests.ts +193 -0
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "@tellescope/sdk",
3
- "version": "0.0.19",
3
+ "version": "0.0.23",
4
4
  "description": "Code for interacting with the Tellescope API",
5
5
  "main": "./lib/cjs/sdk.js",
6
6
  "module": "./lib/esm/sdk.js",
7
7
  "types": "./lib/esm/sdk.d.ts",
8
8
  "scripts": {
9
9
  "test": "npm run-script build:cjs && npm run-script retest",
10
- "retest": "npm run-script retest-sockets && npm run-script retest-api && npm run-script retest-public-api",
10
+ "retest": "npm run-script retest-api && npm run-script retest-public-api && npm run-script retest-webhooks && npm run-script retest-sockets",
11
11
  "test-api": "npm run-script build:cjs && npm run-script retest-api",
12
12
  "retest-api": "node lib/cjs/tests/tests.js",
13
13
  "test-public-api": "npm run-script build:cjs && npm run-script retest-public-api",
14
14
  "retest-public-api": "node lib/cjs/tests/public_endpoint_tests.js",
15
15
  "test-sockets": "npm run-script build:cjs && npm run-script retest-sockets",
16
16
  "retest-sockets": "node lib/cjs/tests/socket_tests.js",
17
+ "test-webhooks": "npm run-script build:cjs && npm run-script retest-webhooks",
18
+ "retest-webhooks": "node lib/cjs/tests/webhooks_tests.js",
17
19
  "build": "npm run-script build:esm && npm run-script build:cjs",
18
20
  "build:esm": "tsc",
19
21
  "build:cjs": "tsc --module commonjs --outDir lib/cjs",
@@ -30,15 +32,16 @@
30
32
  },
31
33
  "homepage": "https://github.com/tellescope-os/tellescope#readme",
32
34
  "dependencies": {
33
- "@tellescope/constants": "^0.0.19",
34
- "@tellescope/schema": "^0.0.19",
35
- "@tellescope/testing": "^0.0.19",
36
- "@tellescope/types-client": "^0.0.19",
37
- "@tellescope/types-models": "^0.0.19",
38
- "@tellescope/types-utilities": "^0.0.19",
39
- "@tellescope/utilities": "^0.0.19",
40
- "@tellescope/validation": "^0.0.19",
35
+ "@tellescope/constants": "^0.0.23",
36
+ "@tellescope/schema": "^0.0.23",
37
+ "@tellescope/testing": "^0.0.23",
38
+ "@tellescope/types-client": "^0.0.23",
39
+ "@tellescope/types-models": "^0.0.23",
40
+ "@tellescope/types-utilities": "^0.0.23",
41
+ "@tellescope/utilities": "^0.0.23",
42
+ "@tellescope/validation": "^0.0.23",
41
43
  "axios": "^0.21.1",
44
+ "express": "^4.17.1",
42
45
  "form-data": "^4.0.0",
43
46
  "socket.io-client": "^4.2.0",
44
47
  "source-map-support": "^0.5.20"
@@ -53,5 +56,5 @@
53
56
  "publishConfig": {
54
57
  "access": "public"
55
58
  },
56
- "gitHead": "ddee8ca945d3daf7355a9eada1305666e56d7434"
59
+ "gitHead": "da81cccdf2fcf06080a5a7d33e0a1cdac9f05a40"
57
60
  }
package/src/enduser.ts CHANGED
@@ -6,14 +6,16 @@ import { url_safe_path } from "@tellescope/utilities"
6
6
 
7
7
  import { S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
8
8
  import {
9
+ Attendee,
9
10
  AttendeeInfo,
10
- Meeting,
11
11
  } from "@tellescope/types-models"
12
12
  import {
13
13
  ClientModelForName,
14
14
  ClientModelForName_required,
15
15
  Enduser,
16
16
  File,
17
+ Meeting,
18
+ UserDisplayInfo,
17
19
  } from "@tellescope/types-client"
18
20
 
19
21
  export interface EnduserSessionOptions extends SessionOptions {}
@@ -43,14 +45,14 @@ type EnduserQueries = { [K in EnduserAccessibleModels]: APIQuery<K> } & {
43
45
  logout: () => Promise<void>;
44
46
  },
45
47
  users: {
46
- display_info: () => Promise<{ fname?: string, lname?: string, id: string, lastActive?: Date, lastLogout?: Date }[]>
48
+ display_info: () => Promise<UserDisplayInfo[]>
47
49
  },
48
50
  files: {
49
51
  prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
50
52
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
51
53
  },
52
54
  meetings: {
53
- attendee_info: (args: { id: string }) => Promise<{ attendee: AttendeeInfo, others: UserIdentity[] }>,
55
+ attendee_info: (args: { id: string }) => Promise<{ attendee: Attendee, others: UserIdentity[] }>,
54
56
  my_meetings: () => Promise<Meeting[]>,
55
57
  },
56
58
  }
@@ -68,8 +70,9 @@ export class EnduserSession extends Session {
68
70
  userInfo!: Enduser;
69
71
  api: EnduserQueries;
70
72
 
71
- constructor(o?: EnduserSessionOptions) {
73
+ constructor(o?: EnduserSessionOptions & { enduser?: Enduser }) {
72
74
  super({ ...o, cacheKey: o?.cacheKey || "tellescope_enduser" })
75
+ if (o?.enduser) this.userInfo = o.enduser
73
76
 
74
77
  this.api = loadDefaultQueries(this) as EnduserQueries
75
78
 
@@ -77,7 +80,7 @@ export class EnduserSession extends Session {
77
80
  logout: () => this._POST('/v1/logout-enduser'),
78
81
  }
79
82
  this.api.users = {
80
- display_info: () => this._GET<{}, { fname: string, lname: string, id: string }[] >(`/v1/user-display-info`),
83
+ display_info: () => this._GET<{}, UserDisplayInfo[] >(`/v1/user-display-info`),
81
84
  }
82
85
  this.api.meetings = {
83
86
  attendee_info: a => this._GET('/v1/attendee-info', a),
package/src/sdk.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  AttendeeInfo,
3
3
  JourneyState,
4
- Meeting,
5
4
  UserSession,
6
5
  MeetingInfo,
7
6
  ReadFilter,
7
+ WebhookSubscriptionsType,
8
+ Attendee,
8
9
  } from "@tellescope/types-models"
9
10
 
10
11
  import {
@@ -15,6 +16,7 @@ import {
15
16
  ChatRoom,
16
17
  Enduser,
17
18
  File,
19
+ Meeting,
18
20
  } from "@tellescope/types-client"
19
21
  import { CustomUpdateOptions, SortOption, S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
20
22
  import { url_safe_path } from "@tellescope/utilities"
@@ -82,6 +84,7 @@ const loadDefaultQueries = (s: Session): { [K in keyof ClientModelForName] : API
82
84
  tickets: defaultQueries(s, 'tickets'),
83
85
  meetings: defaultQueries(s, 'meetings'),
84
86
  notes: defaultQueries(s, 'notes'),
87
+ webhooks: defaultQueries(s, 'webhooks')
85
88
  })
86
89
 
87
90
  type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
@@ -101,23 +104,29 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
101
104
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
102
105
  },
103
106
  meetings: {
104
- start_meeting: () => Promise<{ id: string, meeting: { Meeting: MeetingInfo }, host: { Attendee: AttendeeInfo } }>,
107
+ start_meeting: () => Promise<{ id: string, meeting: { Meeting: MeetingInfo }, host: Attendee }>,
105
108
  end_meeting: (args: { id: string }) => Promise<void>,
106
109
  add_attendees_to_meeting: (args: { id: string, attendees: UserIdentity[] }) => Promise<void>,
107
110
  my_meetings: () => Promise<Meeting[]>,
108
- attendee_info: (args: { id: string }) => Promise<{ attendee: AttendeeInfo, others: UserIdentity[] }>,
111
+ attendee_info: (args: { id: string }) => Promise<{ attendee: Attendee, others: UserIdentity[] }>,
109
112
  },
110
113
  chat_rooms: {
111
114
  join_room: (args: { id: string }) => Promise<{ room: ChatRoom }>,
112
115
  },
116
+ webhooks: {
117
+ configure: (args: { url: string, secret: string, subscriptions?: WebhookSubscriptionsType }) => Promise<void>,
118
+ update: (args: { url?: string, secret?: string, subscriptionUpdates?: WebhookSubscriptionsType }) => Promise<void>
119
+ },
113
120
  }
114
121
 
115
122
  export class Session extends SessionManager {
116
123
  api: Queries;
117
124
  userInfo!: UserSession;
118
125
 
119
- constructor(o?: SessionOptions) {
126
+ constructor(o?: SessionOptions & { userInfo?: UserSession }) {
120
127
  super({ ...o, cacheKey: o?.cacheKey || "tellescope_user" })
128
+ if (o?.userInfo) this.userInfo = o.userInfo
129
+
121
130
  const queries = loadDefaultQueries(this) as Queries
122
131
 
123
132
  queries.journeys.update_state = ({id, name, updates}) => this._PATCH(`/v1/journey/${id}/state/${name}`, { updates })
@@ -135,6 +144,9 @@ export class Session extends SessionManager {
135
144
  queries.meetings.attendee_info = a => this._GET('/v1/attendee-info', a)
136
145
  queries.meetings.my_meetings = () => this._GET('/v1/my-meetings')
137
146
 
147
+ queries.webhooks.configure = a => this._POST('/v1/configure-webhooks', a)
148
+ queries.webhooks.update = a => this._PATCH('/v1/update-webhooks', a)
149
+
138
150
  this.api = queries
139
151
 
140
152
  // if (this.userInfo) this.refresh_session()
package/src/session.ts CHANGED
@@ -3,16 +3,11 @@ import NodeFormData from "form-data"
3
3
  import { Socket, io } from 'socket.io-client'
4
4
 
5
5
  import {
6
- CustomUpdateOptions,
7
6
  FileBlob,
8
- SortOption,
9
7
  S3PresignedPost,
10
8
  } from "@tellescope/types-utilities"
11
9
  import {
12
10
  ClientModelForName,
13
- ClientModelForName_required,
14
- ClientModelForName_readonly,
15
- ClientModelForName_updatesDisabled
16
11
  } from "@tellescope/types-client"
17
12
 
18
13
  export const DEFAULT_HOST = 'https://api.tellescope.com'
@@ -288,13 +288,21 @@ const updatesTests = async () => {
288
288
 
289
289
  const generateEnduserAuthTests = async () => {
290
290
  log_header("Generated Enduser authToken Tests")
291
- const e = await sdk.api.endusers.createOne({ email: 'generated@tellescope.com', phone: '+15555555555', externalId: '1029f9v9sjd0as' })
291
+ const externalId = '1029f9v9sjd0as'
292
+ const e = await sdk.api.endusers.createOne({ email: 'generated@tellescope.com', phone: '+15555555555', externalId })
293
+
292
294
  const { authToken, enduser } = await sdk.api.endusers.generate_auth_token({ id: e.id })
293
295
  assert(!!authToken && !!enduser, 'invalid returned values', 'Generate authToken and get enduser')
294
-
295
296
  const { isAuthenticated } = await sdk.api.endusers.is_authenticated({ id: enduser.id, authToken })
296
297
  assert(isAuthenticated, 'invalid authToken generated for enduser', 'Generate authToken for enduser is valid')
297
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
+
298
306
  await async_test(
299
307
  `auth by externalId`, () => sdk.api.endusers.generate_auth_token({ externalId: e.externalId }), passOnVoid,
300
308
  )
@@ -965,47 +973,48 @@ const enduserAccessTests = async () => {
965
973
  for (const n in schema) {
966
974
  const endpoint = url_safe_path(n)
967
975
  const model = schema[n as keyof typeof schema]
976
+ if (n === 'webhooks') continue // no default endpoints implemented
968
977
 
969
978
  if (!model?.enduserActions?.read && (model.defaultActions.read || model.customActions.read)) {
970
979
  await async_test(
971
980
  `no-enduser-access getOne (${endpoint})`,
972
981
  () => enduserSDK.GET(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
973
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
982
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
974
983
  )
975
984
  }
976
985
  if (!model.enduserActions?.readMany && (model.defaultActions.readMany || model.customActions.readMany)) {
977
986
  await async_test(
978
987
  `no-enduser-access getSome (${endpoint})`,
979
988
  () => enduserSDK.GET(`/v1/${endpoint}`),
980
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
989
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
981
990
  )
982
991
  }
983
992
  if (!model.enduserActions?.create && (model.defaultActions.create || model.customActions.create)) {
984
993
  await async_test(
985
994
  `no-enduser-access createOne (${endpoint})`,
986
995
  () => enduserSDK.POST(`/v1/${endpoint.substring(0, endpoint.length - 1)}`),
987
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
996
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
988
997
  )
989
998
  }
990
999
  if (!model.enduserActions?.createMany && (model.defaultActions.createMany || model.customActions.createMany)) {
991
1000
  await async_test(
992
1001
  `no-enduser-access createMany (${endpoint})`,
993
1002
  () => enduserSDK.POST(`/v1/${endpoint}`),
994
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1003
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
995
1004
  )
996
1005
  }
997
1006
  if (!model.enduserActions?.update && (model.defaultActions.update || model.customActions.update)) {
998
1007
  await async_test(
999
1008
  `no-enduser-access update (${endpoint})`,
1000
1009
  () => enduserSDK.PATCH(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
1001
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1010
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
1002
1011
  )
1003
1012
  }
1004
1013
  if (!model.enduserActions?.delete && (model.defaultActions.delete || model.customActions.delete)) {
1005
1014
  await async_test(
1006
1015
  `no-enduser-access delete (${endpoint})`,
1007
1016
  () => enduserSDK.DELETE(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
1008
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1017
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
1009
1018
  )
1010
1019
  }
1011
1020
  }
@@ -1052,6 +1061,20 @@ const files_tests = async () => {
1052
1061
  assert(downloaded === buff.toString(), 'downloaded file does not match uploaded file', 'upload, download comparison')
1053
1062
  }
1054
1063
 
1064
+ const enduser_session_tests = async () => {
1065
+ const email = 'enduser@tellescope.com'
1066
+ const password = 'testpassword'
1067
+
1068
+ const enduser = await sdk.api.endusers.createOne({ email })
1069
+ await sdk.api.endusers.set_password({ id: enduser.id, password }).catch(console.error)
1070
+ await enduserSDK.authenticate(email, password).catch(console.error)
1071
+
1072
+ const users = await enduserSDK.api.users.display_info()
1073
+ assert(users && users.length > 0, 'No users returned', 'Get user display info for enduser')
1074
+
1075
+ await sdk.api.endusers.deleteOne(enduser.id)
1076
+ }
1077
+
1055
1078
  const tests: { [K in keyof ClientModelForName]: () => void } = {
1056
1079
  chats: chat_tests,
1057
1080
  endusers: enduser_tests,
@@ -1068,6 +1091,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1068
1091
  tickets: () => {},
1069
1092
  meetings: () => {},
1070
1093
  notes: () => {},
1094
+ webhooks: () => {},
1071
1095
  };
1072
1096
 
1073
1097
  (async () => {
@@ -1083,6 +1107,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1083
1107
  await threadKeyTests()
1084
1108
  await enduserAccessTests()
1085
1109
  await generateEnduserAuthTests()
1110
+ await enduser_session_tests()
1086
1111
  } catch(err) {
1087
1112
  console.error("Failed during custom test")
1088
1113
  console.error(err)
@@ -1099,7 +1124,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1099
1124
  model: schema[n] as any,
1100
1125
  name: n,
1101
1126
  returns: {
1102
- create: returnValidation as ModelFields<ClientModel>,
1127
+ create: returnValidation as any// ModelFields<ClientModel>,
1103
1128
  }
1104
1129
  })
1105
1130
  }
@@ -0,0 +1,193 @@
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 empty subscription)',
148
+ () => sdk.api.webhooks.update({ subscriptionUpdates: {} }),
149
+ { onResult: _ => true }
150
+ )
151
+ await async_test(
152
+ 'update webhook (set partial subscription)',
153
+ () => sdk.api.webhooks.update({ subscriptionUpdates: { chats: { create: true }} }),
154
+ { onResult: _ => true }
155
+ )
156
+ await async_test(
157
+ 'update webhook (set subscriptions)',
158
+ () => sdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
159
+ { onResult: _ => true }
160
+ )
161
+ await async_test(
162
+ 'update webhook invalid model',
163
+ () => sdk.api.webhooks.update({ subscriptionUpdates: { notAModel: { create: false } } as any }),
164
+ { shouldError: true, onError: e => e.message === "Error parsing field subscriptionUpdates: Got unexpected field(s) [notAModel]" }
165
+ )
166
+
167
+ log_header("Webhooks Tests with Subscriptions")
168
+ for (const t in tests) {
169
+ await tests[t as keyof typeof tests](true)
170
+ }
171
+ const finalLength = handledEvents.length
172
+
173
+ await async_test(
174
+ 'update webhook (set subscriptions empty)',
175
+ () => sdk.api.webhooks.update({ subscriptionUpdates: emptySubscription }),
176
+ { onResult: _ => true }
177
+ )
178
+
179
+ log_header("Webhooks Tests without Subscriptions")
180
+ for (const t in tests) {
181
+ await tests[t as keyof typeof tests](false)
182
+ }
183
+ assert(finalLength === handledEvents.length, 'length changed after subscriptions', 'No webhooks posted when no subscription')
184
+
185
+ }
186
+
187
+ app.listen(PORT, async () => {
188
+ try {
189
+ await run_tests()
190
+ } catch(err) { console.error(err) }
191
+
192
+ process.exit()
193
+ })