@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.
- package/lib/cjs/enduser.d.ts +166 -14
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/enduser.js +2 -0
- package/lib/cjs/enduser.js.map +1 -1
- package/lib/cjs/sdk.d.ts +174 -10
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +5 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/session.js.map +1 -1
- package/lib/cjs/tests/tests.js +93 -50
- 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 +267 -0
- package/lib/cjs/tests/webhooks_tests.js.map +1 -0
- package/lib/esm/enduser.d.ts +166 -14
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/enduser.js +2 -0
- package/lib/esm/enduser.js.map +1 -1
- package/lib/esm/sdk.d.ts +174 -10
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +5 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/session.js.map +1 -1
- package/lib/esm/tests/tests.js +93 -50
- 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 +262 -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 +8 -5
- package/src/sdk.ts +16 -4
- package/src/session.ts +0 -5
- package/src/tests/tests.ts +34 -9
- 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.
|
|
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-
|
|
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.
|
|
34
|
-
"@tellescope/schema": "^0.0.
|
|
35
|
-
"@tellescope/testing": "^0.0.
|
|
36
|
-
"@tellescope/types-client": "^0.0.
|
|
37
|
-
"@tellescope/types-models": "^0.0.
|
|
38
|
-
"@tellescope/types-utilities": "^0.0.
|
|
39
|
-
"@tellescope/utilities": "^0.0.
|
|
40
|
-
"@tellescope/validation": "^0.0.
|
|
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": "
|
|
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<
|
|
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:
|
|
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<{},
|
|
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:
|
|
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:
|
|
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'
|
package/src/tests/tests.ts
CHANGED
|
@@ -288,13 +288,21 @@ const updatesTests = async () => {
|
|
|
288
288
|
|
|
289
289
|
const generateEnduserAuthTests = async () => {
|
|
290
290
|
log_header("Generated Enduser authToken Tests")
|
|
291
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
})
|