@tellescope/sdk 0.0.20 → 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.
@@ -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
+ })