@tellescope/sdk 0.0.20 → 0.0.24
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 +163 -13
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/enduser.js.map +1 -1
- package/lib/cjs/sdk.d.ts +171 -9
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +3 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/tests.js +64 -31
- 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 +276 -0
- package/lib/cjs/tests/webhooks_tests.js.map +1 -0
- package/lib/esm/enduser.d.ts +163 -13
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/enduser.js.map +1 -1
- package/lib/esm/sdk.d.ts +171 -9
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +3 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/tests.js +64 -31
- 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 +271 -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 +6 -4
- package/src/sdk.ts +13 -3
- package/src/tests/tests.ts +24 -7
- package/src/tests/webhooks_tests.ts +207 -0
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
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-api && npm run-script retest-public-api && npm run-script retest-sockets",
|
|
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.24",
|
|
36
|
+
"@tellescope/schema": "^0.0.24",
|
|
37
|
+
"@tellescope/testing": "^0.0.24",
|
|
38
|
+
"@tellescope/types-client": "^0.0.24",
|
|
39
|
+
"@tellescope/types-models": "^0.0.24",
|
|
40
|
+
"@tellescope/types-utilities": "^0.0.24",
|
|
41
|
+
"@tellescope/utilities": "^0.0.24",
|
|
42
|
+
"@tellescope/validation": "^0.0.24",
|
|
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": "b43edb136f319f3afac336237c7bcb030bb3c60d"
|
|
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
|
}
|
|
@@ -78,7 +80,7 @@ export class EnduserSession extends Session {
|
|
|
78
80
|
logout: () => this._POST('/v1/logout-enduser'),
|
|
79
81
|
}
|
|
80
82
|
this.api.users = {
|
|
81
|
-
display_info: () => this._GET<{},
|
|
83
|
+
display_info: () => this._GET<{}, UserDisplayInfo[] >(`/v1/user-display-info`),
|
|
82
84
|
}
|
|
83
85
|
this.api.meetings = {
|
|
84
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,15 +104,19 @@ 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 {
|
|
@@ -137,6 +144,9 @@ export class Session extends SessionManager {
|
|
|
137
144
|
queries.meetings.attendee_info = a => this._GET('/v1/attendee-info', a)
|
|
138
145
|
queries.meetings.my_meetings = () => this._GET('/v1/my-meetings')
|
|
139
146
|
|
|
147
|
+
queries.webhooks.configure = a => this._POST('/v1/configure-webhooks', a)
|
|
148
|
+
queries.webhooks.update = a => this._PATCH('/v1/update-webhooks', a)
|
|
149
|
+
|
|
140
150
|
this.api = queries
|
|
141
151
|
|
|
142
152
|
// if (this.userInfo) this.refresh_session()
|
package/src/tests/tests.ts
CHANGED
|
@@ -973,47 +973,48 @@ const enduserAccessTests = async () => {
|
|
|
973
973
|
for (const n in schema) {
|
|
974
974
|
const endpoint = url_safe_path(n)
|
|
975
975
|
const model = schema[n as keyof typeof schema]
|
|
976
|
+
if (n === 'webhooks') continue // no default endpoints implemented
|
|
976
977
|
|
|
977
978
|
if (!model?.enduserActions?.read && (model.defaultActions.read || model.customActions.read)) {
|
|
978
979
|
await async_test(
|
|
979
980
|
`no-enduser-access getOne (${endpoint})`,
|
|
980
981
|
() => enduserSDK.GET(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
981
|
-
{ shouldError: true, onError: (e:
|
|
982
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
982
983
|
)
|
|
983
984
|
}
|
|
984
985
|
if (!model.enduserActions?.readMany && (model.defaultActions.readMany || model.customActions.readMany)) {
|
|
985
986
|
await async_test(
|
|
986
987
|
`no-enduser-access getSome (${endpoint})`,
|
|
987
988
|
() => enduserSDK.GET(`/v1/${endpoint}`),
|
|
988
|
-
{ shouldError: true, onError: (e:
|
|
989
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
989
990
|
)
|
|
990
991
|
}
|
|
991
992
|
if (!model.enduserActions?.create && (model.defaultActions.create || model.customActions.create)) {
|
|
992
993
|
await async_test(
|
|
993
994
|
`no-enduser-access createOne (${endpoint})`,
|
|
994
995
|
() => enduserSDK.POST(`/v1/${endpoint.substring(0, endpoint.length - 1)}`),
|
|
995
|
-
{ shouldError: true, onError: (e:
|
|
996
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
996
997
|
)
|
|
997
998
|
}
|
|
998
999
|
if (!model.enduserActions?.createMany && (model.defaultActions.createMany || model.customActions.createMany)) {
|
|
999
1000
|
await async_test(
|
|
1000
1001
|
`no-enduser-access createMany (${endpoint})`,
|
|
1001
1002
|
() => enduserSDK.POST(`/v1/${endpoint}`),
|
|
1002
|
-
{ shouldError: true, onError: (e:
|
|
1003
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
1003
1004
|
)
|
|
1004
1005
|
}
|
|
1005
1006
|
if (!model.enduserActions?.update && (model.defaultActions.update || model.customActions.update)) {
|
|
1006
1007
|
await async_test(
|
|
1007
1008
|
`no-enduser-access update (${endpoint})`,
|
|
1008
1009
|
() => enduserSDK.PATCH(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
1009
|
-
{ shouldError: true, onError: (e:
|
|
1010
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
1010
1011
|
)
|
|
1011
1012
|
}
|
|
1012
1013
|
if (!model.enduserActions?.delete && (model.defaultActions.delete || model.customActions.delete)) {
|
|
1013
1014
|
await async_test(
|
|
1014
1015
|
`no-enduser-access delete (${endpoint})`,
|
|
1015
1016
|
() => enduserSDK.DELETE(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
|
|
1016
|
-
{ shouldError: true, onError: (e:
|
|
1017
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
|
|
1017
1018
|
)
|
|
1018
1019
|
}
|
|
1019
1020
|
}
|
|
@@ -1060,6 +1061,20 @@ const files_tests = async () => {
|
|
|
1060
1061
|
assert(downloaded === buff.toString(), 'downloaded file does not match uploaded file', 'upload, download comparison')
|
|
1061
1062
|
}
|
|
1062
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
|
+
|
|
1063
1078
|
const tests: { [K in keyof ClientModelForName]: () => void } = {
|
|
1064
1079
|
chats: chat_tests,
|
|
1065
1080
|
endusers: enduser_tests,
|
|
@@ -1076,6 +1091,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
|
|
|
1076
1091
|
tickets: () => {},
|
|
1077
1092
|
meetings: () => {},
|
|
1078
1093
|
notes: () => {},
|
|
1094
|
+
webhooks: () => {},
|
|
1079
1095
|
};
|
|
1080
1096
|
|
|
1081
1097
|
(async () => {
|
|
@@ -1091,6 +1107,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
|
|
|
1091
1107
|
await threadKeyTests()
|
|
1092
1108
|
await enduserAccessTests()
|
|
1093
1109
|
await generateEnduserAuthTests()
|
|
1110
|
+
await enduser_session_tests()
|
|
1094
1111
|
} catch(err) {
|
|
1095
1112
|
console.error("Failed during custom test")
|
|
1096
1113
|
console.error(err)
|
|
@@ -1107,7 +1124,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
|
|
|
1107
1124
|
model: schema[n] as any,
|
|
1108
1125
|
name: n,
|
|
1109
1126
|
returns: {
|
|
1110
|
-
create: returnValidation as ModelFields<ClientModel>,
|
|
1127
|
+
create: returnValidation as any// ModelFields<ClientModel>,
|
|
1111
1128
|
}
|
|
1112
1129
|
})
|
|
1113
1130
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
import { ChatMessage } from "@tellescope/types-client"
|
|
25
|
+
|
|
26
|
+
const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
|
|
27
|
+
const [email2, password2] = [process.env.TEST_EMAIL_2, process.env.TEST_PASSWORD_2]
|
|
28
|
+
const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
|
|
29
|
+
if (!(email && password && email2 && password2 && nonAdminEmail && nonAdminPassword)) {
|
|
30
|
+
console.error("Set TEST_EMAIL and TEST_PASSWORD")
|
|
31
|
+
process.exit()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const sdk = new Session({ host: 'http://localhost:8080' })
|
|
36
|
+
const nonAdminSdk = new Session({ host: 'http://localhost:8080' })
|
|
37
|
+
|
|
38
|
+
const app = express()
|
|
39
|
+
app.use(bodyParser.urlencoded({ extended: true, limit: '25mb' }))
|
|
40
|
+
app.use(bodyParser.json({ limit: "25mb" }))
|
|
41
|
+
|
|
42
|
+
const PORT = 4000
|
|
43
|
+
const TEST_SECRET = "this is a test secret for verifying integrity of web hooks"
|
|
44
|
+
const webhookEndpoint = '/handle-webhook'
|
|
45
|
+
const webhookURL = `http://127.0.0.1:${PORT}${webhookEndpoint}`
|
|
46
|
+
|
|
47
|
+
const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex')
|
|
48
|
+
|
|
49
|
+
const verify_integrity = (records: WebhookRecord[], timestamp: string, integrity: string,) => (
|
|
50
|
+
sha256(records.map(r => r.id).join('') + timestamp + TEST_SECRET) === integrity
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const handledEvents: WebhookCall[] = []
|
|
54
|
+
app.post(webhookEndpoint, (req, res) => {
|
|
55
|
+
const body = req.body as WebhookCall
|
|
56
|
+
// console.log('got hook', body.records, body.timestamp, body.integrity)
|
|
57
|
+
|
|
58
|
+
if (!verify_integrity(body.records, body.timestamp, body.integrity)) {
|
|
59
|
+
console.error("Integrity check failed for request", JSON.stringify(body, null, 2))
|
|
60
|
+
process.exit()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
handledEvents.push(req.body)
|
|
64
|
+
res.status(204).end()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const fullSubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
|
|
68
|
+
const emptySubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
|
|
69
|
+
for (const model in WEBHOOK_MODELS) {
|
|
70
|
+
fullSubscription[model as WebhookSupportedModel] = { create: true, update: true, delete: true }
|
|
71
|
+
emptySubscription[model as WebhookSupportedModel] = { create: false, update: false, delete: false }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let webhookIndex = 0
|
|
75
|
+
const check_next_webhook = async (evaluate: (hook: WebhookCall) => boolean, name: string, error: string, isSubscribed: boolean) => {
|
|
76
|
+
if (isSubscribed === false) return
|
|
77
|
+
|
|
78
|
+
await wait(undefined, 25) // wait for hook to post
|
|
79
|
+
|
|
80
|
+
const event = handledEvents[webhookIndex]
|
|
81
|
+
assert(!!event, 'did not get hook', 'got hook')
|
|
82
|
+
if (!event) return // ensure webhookIndex not incremented
|
|
83
|
+
|
|
84
|
+
const success = evaluate(event)
|
|
85
|
+
assert(success, error, name)
|
|
86
|
+
if (!success) { console.error('Got', event) }
|
|
87
|
+
|
|
88
|
+
webhookIndex++
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const chats_tests = async (isSubscribed: boolean) => {
|
|
92
|
+
log_header(`Chats Tests, isSubscribed=${isSubscribed}`)
|
|
93
|
+
const room = await sdk.api.chat_rooms.createOne({ userIds: [sdk.userInfo.id] })
|
|
94
|
+
|
|
95
|
+
const chat = await sdk.api.chats.createOne({ roomId: room.id, message: "Hello hello hi hello" })
|
|
96
|
+
await check_next_webhook(
|
|
97
|
+
({ records, relatedRecords }) => {
|
|
98
|
+
const record = records[0] as ChatMessage
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
objects_equivalent(record, chat) &&
|
|
102
|
+
relatedRecords[record.roomId] !== undefined &&
|
|
103
|
+
relatedRecords[record.senderId as string] !== undefined &&
|
|
104
|
+
relatedRecords[record.roomId]?.id === room.id &&
|
|
105
|
+
relatedRecords[record.senderId as string]?.id === room.userIds?.[0]
|
|
106
|
+
)
|
|
107
|
+
},
|
|
108
|
+
'Create chat error', 'Create chat webhook', isSubscribed
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// cleanup
|
|
112
|
+
await sdk.api.chat_rooms.deleteOne(room.id) // also cleans up messages
|
|
113
|
+
|
|
114
|
+
// when chatroom support added for webhooks, check deletion here
|
|
115
|
+
// await check_next_webhook(a => objects_equivalent(a.records, [chat_room]), 'Delete chat room error', 'Delete chat room webhook', isSubscribed)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const meetings_tests = async (isSubscribed: boolean) => {
|
|
119
|
+
log_header(`Meetings Tests, isSubscribed=${isSubscribed}`)
|
|
120
|
+
const meeting = await sdk.api.meetings.start_meeting()
|
|
121
|
+
|
|
122
|
+
await check_next_webhook(a => objects_equivalent(a.records, [meeting]), 'Create meeting error', 'Create meeting webhook', isSubscribed)
|
|
123
|
+
|
|
124
|
+
// cleanup
|
|
125
|
+
await sdk.api.meetings.end_meeting({ id: meeting.id }) // also cleans up messages
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tests: { [K in WebhookSupportedModel]: (isSubscribed: boolean) => Promise<void> } = {
|
|
129
|
+
chats: chats_tests,
|
|
130
|
+
meetings: meetings_tests,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const run_tests = async () => {
|
|
134
|
+
log_header("Webhooks Tests")
|
|
135
|
+
await sdk.authenticate(email, password)
|
|
136
|
+
await sdk.reset_db()
|
|
137
|
+
await nonAdminSdk.authenticate(nonAdminEmail, nonAdminPassword)
|
|
138
|
+
|
|
139
|
+
await async_test(
|
|
140
|
+
'configure webhook is admin only',
|
|
141
|
+
() => nonAdminSdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
142
|
+
{ shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
|
|
143
|
+
)
|
|
144
|
+
await async_test(
|
|
145
|
+
'update webhook is admin only',
|
|
146
|
+
() => nonAdminSdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
|
|
147
|
+
{ shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
await async_test(
|
|
151
|
+
'configure webhook',
|
|
152
|
+
() => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
153
|
+
{ onResult: _ => true }
|
|
154
|
+
)
|
|
155
|
+
await async_test(
|
|
156
|
+
'configure webhook (only callable once)',
|
|
157
|
+
() => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
|
|
158
|
+
{ shouldError: true, onError: e => e.message === "Only one webhook configuration is supported per organization. Use /update-webooks to update your configuration." }
|
|
159
|
+
)
|
|
160
|
+
await async_test(
|
|
161
|
+
'update webhook (set empty subscription)',
|
|
162
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: {} }),
|
|
163
|
+
{ onResult: _ => true }
|
|
164
|
+
)
|
|
165
|
+
await async_test(
|
|
166
|
+
'update webhook (set partial subscription)',
|
|
167
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: { chats: { create: true }} }),
|
|
168
|
+
{ onResult: _ => true }
|
|
169
|
+
)
|
|
170
|
+
await async_test(
|
|
171
|
+
'update webhook (set subscriptions)',
|
|
172
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
|
|
173
|
+
{ onResult: _ => true }
|
|
174
|
+
)
|
|
175
|
+
await async_test(
|
|
176
|
+
'update webhook invalid model',
|
|
177
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: { notAModel: { create: false } } as any }),
|
|
178
|
+
{ shouldError: true, onError: e => e.message === "Error parsing field subscriptionUpdates: Got unexpected field(s) [notAModel]" }
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
log_header("Webhooks Tests with Subscriptions")
|
|
182
|
+
for (const t in tests) {
|
|
183
|
+
await tests[t as keyof typeof tests](true)
|
|
184
|
+
}
|
|
185
|
+
const finalLength = handledEvents.length
|
|
186
|
+
|
|
187
|
+
await async_test(
|
|
188
|
+
'update webhook (set subscriptions empty)',
|
|
189
|
+
() => sdk.api.webhooks.update({ subscriptionUpdates: emptySubscription }),
|
|
190
|
+
{ onResult: _ => true }
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
log_header("Webhooks Tests without Subscriptions")
|
|
194
|
+
for (const t in tests) {
|
|
195
|
+
await tests[t as keyof typeof tests](false)
|
|
196
|
+
}
|
|
197
|
+
assert(finalLength === handledEvents.length, 'length changed after subscriptions', 'No webhooks posted when no subscription')
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
app.listen(PORT, async () => {
|
|
202
|
+
try {
|
|
203
|
+
await run_tests()
|
|
204
|
+
} catch(err) { console.error(err) }
|
|
205
|
+
|
|
206
|
+
process.exit()
|
|
207
|
+
})
|