@tellescope/sdk 1.254.1 → 1.255.0

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 (38) hide show
  1. package/lib/cjs/sdk.d.ts +4 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +5 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js +254 -0
  8. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/organization_api_keys.test.js +226 -0
  12. package/lib/cjs/tests/api_tests/organization_api_keys.test.js.map +1 -0
  13. package/lib/cjs/tests/tests.d.ts.map +1 -1
  14. package/lib/cjs/tests/tests.js +503 -262
  15. package/lib/cjs/tests/tests.js.map +1 -1
  16. package/lib/esm/sdk.d.ts +4 -0
  17. package/lib/esm/sdk.d.ts.map +1 -1
  18. package/lib/esm/sdk.js +5 -0
  19. package/lib/esm/sdk.js.map +1 -1
  20. package/lib/esm/session.d.ts +1 -0
  21. package/lib/esm/session.d.ts.map +1 -1
  22. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  23. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  24. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js +250 -0
  25. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  26. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  27. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  28. package/lib/esm/tests/api_tests/organization_api_keys.test.js +222 -0
  29. package/lib/esm/tests/api_tests/organization_api_keys.test.js.map +1 -0
  30. package/lib/esm/tests/tests.d.ts.map +1 -1
  31. package/lib/esm/tests/tests.js +503 -262
  32. package/lib/esm/tests/tests.js.map +1 -1
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +10 -10
  35. package/src/sdk.ts +11 -0
  36. package/src/tests/api_tests/organization_api_keys.test.ts +207 -0
  37. package/src/tests/tests.ts +189 -2
  38. package/test_generated.pdf +0 -0
@@ -0,0 +1,207 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ assert,
6
+ async_test,
7
+ log_header,
8
+ wait,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ // fields that the admin org-wide list endpoint is allowed to return
15
+ const ALLOWED_FIELDS = ['id', 'creator', 'businessId', 'updatedAt'].sort()
16
+
17
+ export const organization_api_keys_tests = async (
18
+ { sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }
19
+ ) => {
20
+ log_header("Organization API Keys (admin org-wide management)")
21
+
22
+ // We never mutate sdkNonAdmin's roles here (throwaway-user rule). Instead we create a
23
+ // dedicated throwaway user in the SAME org that DOES have api_keys access (via a custom
24
+ // role), so it can create its own key (the offboarded-employee scenario) and so the
25
+ // admin-gating negative tests below exercise the adminOnly gate (401) rather than the
26
+ // access-permission gate (403) — a non-admin without api_keys access is rejected with
27
+ // "Inaccessible" before adminOnly is ever reached.
28
+ const adminKeyIds: string[] = []
29
+ let otherKeyId: string | undefined
30
+ let otherUserId: string | undefined
31
+ let apiKeysRoleId: string | undefined
32
+
33
+ try {
34
+ // A custom role that grants api_keys access (merged over PROVIDER_PERMISSIONS server-side)
35
+ const apiKeysRole = await sdk.api.role_based_access_permissions.createOne({
36
+ role: 'api-keys-test-role',
37
+ permissions: { api_keys: { create: 'All', read: 'All', update: 'All', delete: 'All' } },
38
+ })
39
+ apiKeysRoleId = apiKeysRole.id
40
+
41
+ // A throwaway non-admin user in the same org, holding that role
42
+ const otherUser = (
43
+ await sdk.api.users.getOne({ email: 'api.keys.other.user@tellescope.com' }).catch(() => null)
44
+ ) || (
45
+ await sdk.api.users.createOne({ email: 'api.keys.other.user@tellescope.com', notificationEmailsDisabled: true, verifiedEmail: true })
46
+ )
47
+ otherUserId = otherUser.id
48
+ await sdk.api.users.updateOne(otherUser.id, { roles: [apiKeysRole.role] }, { replaceObjectFields: true })
49
+ await wait(undefined, 2000) // wait for the role change to propagate before authenticating
50
+
51
+ const otherSdk = new Session({
52
+ host,
53
+ authToken: (await sdk.api.users.generate_auth_token({ id: otherUser.id })).authToken,
54
+ })
55
+
56
+ // A key created by the admin...
57
+ const adminKey = await sdk.api.api_keys.createOne({})
58
+ adminKeyIds.push(adminKey.id)
59
+
60
+ // ...and a key created by ANOTHER user in the org (the offboarded-employee scenario)
61
+ const otherKey = await otherSdk.api.api_keys.createOne({})
62
+ otherKeyId = otherKey.id
63
+
64
+ // =============================================
65
+ // CORE: admin sees keys created by other users
66
+ // =============================================
67
+ const { apiKeys } = await sdk.api.api_keys.get_organization_api_keys()
68
+
69
+ assert(
70
+ apiKeys.some(k => k.id === otherKeyId),
71
+ `admin list did not include another user's key ${otherKeyId}`,
72
+ "admin get_organization_api_keys returns keys created by other users in the org"
73
+ )
74
+ assert(
75
+ apiKeys.some(k => k.id === adminKey.id),
76
+ "admin list did not include the admin's own key",
77
+ "admin get_organization_api_keys returns the admin's own keys"
78
+ )
79
+ assert(
80
+ !!apiKeys.find(k => k.id === otherKeyId && k.creator === otherUser.id),
81
+ "creator on the other user's key did not match",
82
+ "returned key reports the correct creator"
83
+ )
84
+
85
+ // =============================================
86
+ // SECRET REDACTION (server-side, allowlist)
87
+ // =============================================
88
+ assert(
89
+ apiKeys.every(k => (k as any).key === undefined),
90
+ "a returned record exposed a plaintext `key`",
91
+ "no returned record exposes a plaintext key (legacy field)"
92
+ )
93
+ assert(
94
+ apiKeys.every(k => (k as any).hashedKey === undefined),
95
+ "a returned record exposed `hashedKey`",
96
+ "no returned record exposes hashedKey"
97
+ )
98
+ // Strict allowlist: every returned field must be in the allowlist (no extra/sensitive field
99
+ // ever leaks). We can't require all allowlisted fields to be PRESENT — never-updated keys
100
+ // have no `updatedAt`, and res.json() drops undefined values — so this is a subset check.
101
+ const offending = apiKeys
102
+ .map(k => Object.keys(k).filter(key => !ALLOWED_FIELDS.includes(key)))
103
+ .find(extra => extra.length > 0)
104
+ assert(
105
+ offending === undefined,
106
+ `a returned record exposed non-allowlisted fields: ${JSON.stringify(offending)}`,
107
+ "returned records contain only allowlisted fields (subset of id, creator, businessId, updatedAt)"
108
+ )
109
+
110
+ // =============================================
111
+ // CROSS-ORG ISOLATION (no leakage across businesses)
112
+ // =============================================
113
+ assert(
114
+ apiKeys.every(k => k.businessId === sdk.userInfo.businessId),
115
+ "a returned key belonged to a different business",
116
+ "all returned keys are scoped to the admin's businessId"
117
+ )
118
+
119
+ // =============================================
120
+ // ADMIN GATING (negative tests)
121
+ // =============================================
122
+ await async_test(
123
+ "non-admin get_organization_api_keys is rejected (401)",
124
+ () => otherSdk.api.api_keys.get_organization_api_keys(),
125
+ { shouldError: true, onError: (e: any) => e.statusCode === 401 || /admin access only/i.test(e.message || '') }
126
+ )
127
+ await async_test(
128
+ "non-admin delete_organization_api_key is rejected (401)",
129
+ () => otherSdk.api.api_keys.delete_organization_api_key({ id: adminKey.id }),
130
+ { shouldError: true, onError: (e: any) => e.statusCode === 401 || /admin access only/i.test(e.message || '') }
131
+ )
132
+
133
+ // confirm the gating did not delete the key
134
+ const afterGating = await sdk.api.api_keys.get_organization_api_keys()
135
+ assert(
136
+ afterGating.apiKeys.some(k => k.id === adminKey.id),
137
+ "rejected non-admin delete still removed the key",
138
+ "rejected non-admin delete left the key intact"
139
+ )
140
+
141
+ // =============================================
142
+ // ADMIN DELETE another user's key
143
+ // =============================================
144
+ await async_test(
145
+ "admin delete_organization_api_key deletes another user's key",
146
+ () => sdk.api.api_keys.delete_organization_api_key({ id: otherKeyId! }),
147
+ { shouldError: false, onResult: () => true }
148
+ )
149
+
150
+ const afterDelete = await sdk.api.api_keys.get_organization_api_keys()
151
+ assert(
152
+ !afterDelete.apiKeys.some(k => k.id === otherKeyId),
153
+ "deleted key still present in org-wide list",
154
+ "deleted key no longer appears in the org-wide list"
155
+ )
156
+ otherKeyId = undefined // successfully deleted; nothing to clean up
157
+
158
+ // deleting a key in a different org (fabricated id) must be a no-op for our org
159
+ await async_test(
160
+ "admin delete of a non-existent/cross-org key id is a safe no-op",
161
+ () => sdk.api.api_keys.delete_organization_api_key({ id: '000000000000000000000000' }),
162
+ { shouldError: false, onResult: () => true }
163
+ )
164
+ const afterNoop = await sdk.api.api_keys.get_organization_api_keys()
165
+ assert(
166
+ afterNoop.apiKeys.some(k => k.id === adminKey.id),
167
+ "no-op delete removed an unrelated key",
168
+ "no-op cross-org delete left our org's keys intact"
169
+ )
170
+ } finally {
171
+ // cleanup: remove any keys we created (creator-only default delete)
172
+ for (const id of adminKeyIds) {
173
+ await sdk.api.api_keys.deleteOne(id).catch(() => {})
174
+ }
175
+ // the admin can clean up the throwaway user's key org-wide; fall through is harmless
176
+ if (otherKeyId) {
177
+ await sdk.api.api_keys.delete_organization_api_key({ id: otherKeyId }).catch(() => {})
178
+ }
179
+ if (otherUserId) {
180
+ await sdk.api.users.deleteOne(otherUserId).catch(() => {})
181
+ }
182
+ if (apiKeysRoleId) {
183
+ await sdk.api.role_based_access_permissions.deleteOne(apiKeysRoleId).catch(() => {})
184
+ }
185
+ }
186
+ }
187
+
188
+ // Allow running this test file independently
189
+ if (require.main === module) {
190
+ const sdk = new Session({ host })
191
+ const sdkNonAdmin = new Session({ host })
192
+
193
+ const runTests = async () => {
194
+ await setup_tests(sdk, sdkNonAdmin)
195
+ await organization_api_keys_tests({ sdk, sdkNonAdmin })
196
+ }
197
+
198
+ runTests()
199
+ .then(() => {
200
+ console.log("✅ Organization API keys test suite completed successfully")
201
+ process.exit(0)
202
+ })
203
+ .catch((error) => {
204
+ console.error("❌ Organization API keys test suite failed:", error)
205
+ process.exit(1)
206
+ })
207
+ }
@@ -80,6 +80,7 @@ import { enduser_login_tests } from "./api_tests/enduser_login.test";
80
80
  import { enduser_login_rate_limits_tests } from "./api_tests/enduser_login_rate_limits.test";
81
81
  import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test";
82
82
  import { cross_org_api_key_tests } from "./api_tests/cross_org_api_key.test";
83
+ import { organization_api_keys_tests } from "./api_tests/organization_api_keys.test";
83
84
  import { custom_dashboards_tests } from "./api_tests/custom_dashboards.test";
84
85
  import { message_assignment_trigger_tests } from "./api_tests/message_assignment_trigger.test";
85
86
  import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent_trigger.test";
@@ -3493,6 +3494,39 @@ const order_status_equals_tests = async () => {
3493
3494
  title: "Protocol Condition"
3494
3495
  })
3495
3496
 
3497
+ // Frequency filter triggers (exact-match array filter, mirrors fills/skus). All use a dedicated
3498
+ // source/status so their assertions stay isolated from the t1-t9 incremental tag-count checks above.
3499
+ const tf1 = await sdk.api.automation_triggers.createOne({
3500
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus" } }, // no frequencies -> fires for all
3501
+ action: { type: 'Add Tags', info: { tags: ['Freq No Filter'] }},
3502
+ status: 'Active',
3503
+ title: "Frequency - no filter (backward compat)"
3504
+ })
3505
+ const tf2 = await sdk.api.automation_triggers.createOne({
3506
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['4', '12'] } },
3507
+ action: { type: 'Add Tags', info: { tags: ['Freq Match'] }},
3508
+ status: 'Active',
3509
+ title: "Frequency - multi-value match"
3510
+ })
3511
+ const tf3 = await sdk.api.automation_triggers.createOne({
3512
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: [] } }, // empty array -> fires for all
3513
+ action: { type: 'Add Tags', info: { tags: ['Freq Empty All'] }},
3514
+ status: 'Active',
3515
+ title: "Frequency - empty array means all"
3516
+ })
3517
+ const tf4 = await sdk.api.automation_triggers.createOne({
3518
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqShipped", frequencies: ['4'] } },
3519
+ action: { type: 'Add Tags', info: { tags: ['Freq Status Combo'] }},
3520
+ status: 'Active',
3521
+ title: "Frequency + status combination (AND semantics)"
3522
+ })
3523
+ const tf5 = await sdk.api.automation_triggers.createOne({
3524
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['12'] } },
3525
+ action: { type: 'Add Tags', info: { tags: ['Freq Match 12'] }},
3526
+ status: 'Active',
3527
+ title: "Frequency - second listed value"
3528
+ })
3529
+
3496
3530
  const e = await sdk.api.endusers.createOne({})
3497
3531
 
3498
3532
  await sdk.api.enduser_orders.createOne({ status: 'Nooo', source: 'Source', title: 'nomatch', externalId: '1', enduserId: e.id })
@@ -3705,6 +3739,85 @@ const order_status_equals_tests = async () => {
3705
3739
  ) }
3706
3740
  )
3707
3741
 
3742
+ // --- Frequency filter coverage (fresh enduser keeps tag-count assertions isolated) ---
3743
+ const ef = await sdk.api.endusers.createOne({})
3744
+
3745
+ // Order with NO frequency: no-filter trigger (tf1) and empty-array trigger (tf3) both fire;
3746
+ // populated-filter trigger (tf2) does NOT (missing frequency must never match a populated filter).
3747
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f1', enduserId: ef.id, title: "Freq No Frequency" })
3748
+ await wait(undefined, 500) // allow triggers to happen
3749
+ await async_test(
3750
+ "Frequency: no-filter + empty-array fire for order with no frequency (backward compat)",
3751
+ () => sdk.api.endusers.getOne(ef.id),
3752
+ { onResult: e => !!(
3753
+ e.tags?.length === 2
3754
+ && e.tags?.includes('Freq No Filter')
3755
+ && e.tags?.includes('Freq Empty All')
3756
+ && !e.tags?.includes('Freq Match')
3757
+ ) }
3758
+ )
3759
+
3760
+ // Order with a frequency NOT in ['4','12'] -> populated-filter trigger still does not fire.
3761
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f2', enduserId: ef.id, title: "Freq 8", frequency: '8' })
3762
+ await wait(undefined, 500) // allow triggers to happen
3763
+ await async_test(
3764
+ "Frequency: no match (order frequency '8' not in ['4','12'])",
3765
+ () => sdk.api.endusers.getOne(ef.id),
3766
+ { onResult: e => !!(
3767
+ e.tags?.length === 2
3768
+ && !e.tags?.includes('Freq Match')
3769
+ ) }
3770
+ )
3771
+
3772
+ // Order with frequency '4' -> ['4','12'] trigger fires.
3773
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f3', enduserId: ef.id, title: "Freq 4", frequency: '4' })
3774
+ await wait(undefined, 500) // allow triggers to happen
3775
+ await async_test(
3776
+ "Frequency: match (order frequency '4' is in ['4','12'])",
3777
+ () => sdk.api.endusers.getOne(ef.id),
3778
+ { onResult: e => !!(
3779
+ e.tags?.length === 3
3780
+ && e.tags?.includes('Freq Match')
3781
+ && !e.tags?.includes('Freq Match 12')
3782
+ ) }
3783
+ )
3784
+
3785
+ // Order with frequency '12' -> the second listed value also matches (tf5 frequencies: ['12']).
3786
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f4', enduserId: ef.id, title: "Freq 12", frequency: '12' })
3787
+ await wait(undefined, 500) // allow triggers to happen
3788
+ await async_test(
3789
+ "Frequency: second listed value '12' also matches",
3790
+ () => sdk.api.endusers.getOne(ef.id),
3791
+ { onResult: e => !!(
3792
+ e.tags?.length === 4
3793
+ && e.tags?.includes('Freq Match 12')
3794
+ ) }
3795
+ )
3796
+
3797
+ // AND semantics: frequency + status must BOTH match. Right frequency, wrong status -> no fire.
3798
+ await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f5', enduserId: ef.id, title: "Freq Combo Wrong Freq", frequency: '8' })
3799
+ await wait(undefined, 500) // allow triggers to happen
3800
+ await async_test(
3801
+ "Frequency + status combo: status matches but frequency does not -> no fire",
3802
+ () => sdk.api.endusers.getOne(ef.id),
3803
+ { onResult: e => !!(
3804
+ e.tags?.length === 4
3805
+ && !e.tags?.includes('Freq Status Combo')
3806
+ ) }
3807
+ )
3808
+
3809
+ // AND semantics: right status AND right frequency -> fires.
3810
+ await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f6', enduserId: ef.id, title: "Freq Combo Match", frequency: '4' })
3811
+ await wait(undefined, 500) // allow triggers to happen
3812
+ await async_test(
3813
+ "Frequency + status combo: both match -> fires (AND semantics)",
3814
+ () => sdk.api.endusers.getOne(ef.id),
3815
+ { onResult: e => !!(
3816
+ e.tags?.length === 5
3817
+ && e.tags?.includes('Freq Status Combo')
3818
+ ) }
3819
+ )
3820
+
3708
3821
  await Promise.all([
3709
3822
  sdk.api.automation_triggers.deleteOne(t1.id),
3710
3823
  sdk.api.automation_triggers.deleteOne(t2.id),
@@ -3715,7 +3828,13 @@ const order_status_equals_tests = async () => {
3715
3828
  sdk.api.automation_triggers.deleteOne(t7.id),
3716
3829
  sdk.api.automation_triggers.deleteOne(t8.id),
3717
3830
  sdk.api.automation_triggers.deleteOne(t9.id),
3831
+ sdk.api.automation_triggers.deleteOne(tf1.id),
3832
+ sdk.api.automation_triggers.deleteOne(tf2.id),
3833
+ sdk.api.automation_triggers.deleteOne(tf3.id),
3834
+ sdk.api.automation_triggers.deleteOne(tf4.id),
3835
+ sdk.api.automation_triggers.deleteOne(tf5.id),
3718
3836
  sdk.api.endusers.deleteOne(e.id),
3837
+ sdk.api.endusers.deleteOne(ef.id),
3719
3838
  ])
3720
3839
  }
3721
3840
 
@@ -6717,11 +6836,25 @@ export const self_serve_appointment_booking_tests = async () => {
6717
6836
  confirmationEmailDisabled: true,
6718
6837
  confirmationSMSDisabled: true,
6719
6838
  })
6720
- const event30minGroup = await sdk.api.calendar_event_templates.createOne({
6839
+ const event30minGroup = await sdk.api.calendar_event_templates.createOne({
6721
6840
  title: 'test group', durationInMinutes: 30,
6722
6841
  confirmationEmailDisabled: true,
6723
6842
  confirmationSMSDisabled: true,
6724
- enduserAttendeeLimit: 2,
6843
+ enduserAttendeeLimit: 2,
6844
+ })
6845
+ const event30minTwilio = await sdk.api.calendar_event_templates.createOne({
6846
+ title: 'test twilio', durationInMinutes: 30,
6847
+ confirmationEmailDisabled: true,
6848
+ confirmationSMSDisabled: true,
6849
+ enableVideoCall: true,
6850
+ videoIntegration: 'Twilio',
6851
+ })
6852
+ const event30minZoom = await sdk.api.calendar_event_templates.createOne({
6853
+ title: 'test zoom', durationInMinutes: 30,
6854
+ confirmationEmailDisabled: true,
6855
+ confirmationSMSDisabled: true,
6856
+ enableVideoCall: true,
6857
+ videoIntegration: 'Zoom',
6725
6858
  })
6726
6859
 
6727
6860
  // ensure it doesn't match current day, to avoid errors on testing
@@ -7281,17 +7414,70 @@ export const self_serve_appointment_booking_tests = async () => {
7281
7414
  { onResult: event => !!event.rescheduledAt }
7282
7415
  )
7283
7416
 
7417
+ // videoIntegration passthrough from template to booked event (Twilio fix + Zoom regression guard)
7418
+ // set up fresh, dedicated availability so this doesn't interfere with the slot-accounting tests above
7419
+ await sdk.api.users.updateOne(sdk.userInfo.id, {
7420
+ weeklyAvailabilities: [
7421
+ {
7422
+ dayOfWeekStartingSundayIndexedByZero,
7423
+ startTimeInMinutes: 60 * 9, // 9am,
7424
+ endTimeInMinutes: 60 * 11, // 11am,
7425
+ },
7426
+ ],
7427
+ timezone: 'America/New_York',
7428
+ }, {
7429
+ replaceObjectFields: true,
7430
+ })
7431
+
7432
+ const twilioSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
7433
+ calendarEventTemplateId: event30minTwilio.id,
7434
+ from: new Date(Date.now() - 10000),
7435
+ to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
7436
+ userIds: [sdk.userInfo.id],
7437
+ })
7438
+ const bookedTwilioAppointment = (await enduserSDK.api.calendar_events.book_appointment({
7439
+ calendarEventTemplateId: event30minTwilio.id,
7440
+ startTime: new Date(twilioSlots.availabilityBlocks[0].startTimeInMS),
7441
+ userId: sdk.userInfo.id,
7442
+ })).createdEvent
7443
+ assert(
7444
+ bookedTwilioAppointment.videoIntegration === 'Twilio',
7445
+ 'Twilio videoIntegration not propagated to booked event',
7446
+ 'Twilio videoIntegration propagated to booked event'
7447
+ )
7448
+
7449
+ const zoomSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
7450
+ calendarEventTemplateId: event30minZoom.id,
7451
+ from: new Date(Date.now() - 10000),
7452
+ to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
7453
+ userIds: [sdk.userInfo.id],
7454
+ })
7455
+ const bookedZoomAppointment = (await enduserSDK.api.calendar_events.book_appointment({
7456
+ calendarEventTemplateId: event30minZoom.id,
7457
+ startTime: new Date(zoomSlots.availabilityBlocks[0].startTimeInMS),
7458
+ userId: sdk.userInfo.id,
7459
+ })).createdEvent
7460
+ assert(
7461
+ bookedZoomAppointment.videoIntegration === 'Zoom',
7462
+ 'Zoom videoIntegration not propagated to booked event',
7463
+ 'Zoom videoIntegration propagated to booked event'
7464
+ )
7465
+
7284
7466
  await Promise.all([
7285
7467
  sdk.api.endusers.deleteOne(e1.id),
7286
7468
  sdk.api.endusers.deleteOne(e2.id),
7287
7469
  sdk.api.endusers.deleteOne(e3.id),
7288
7470
  sdk.api.calendar_event_templates.deleteOne(event30min.id),
7289
7471
  sdk.api.calendar_event_templates.deleteOne(event30minGroup.id),
7472
+ sdk.api.calendar_event_templates.deleteOne(event30minTwilio.id),
7473
+ sdk.api.calendar_event_templates.deleteOne(event30minZoom.id),
7290
7474
  sdk.api.calendar_event_templates.deleteOne(event15min.id),
7291
7475
  sdk.api.calendar_event_templates.deleteOne(templatePreserveHosts.id),
7292
7476
  sdk.api.calendar_event_templates.deleteOne(templateReplaceHosts.id),
7293
7477
  sdk.api.calendar_events.deleteOne(bookedAppointment.id),
7294
7478
  sdk.api.calendar_events.deleteOne(bookedMultiAppointment.id),
7479
+ sdk.api.calendar_events.deleteOne(bookedTwilioAppointment.id),
7480
+ sdk.api.calendar_events.deleteOne(bookedZoomAppointment.id),
7295
7481
  sdk.api.calendar_events.deleteOne(groupEvent.id),
7296
7482
  sdk.api.calendar_events.deleteOne(originalMultiHostAppt.id),
7297
7483
  sdk.api.calendar_events.deleteOne(rescheduledPreserveHosts.id),
@@ -14870,6 +15056,7 @@ const ip_address_form_tests = async () => {
14870
15056
  await enduser_cross_access_isolation_tests({ sdk, sdkNonAdmin })
14871
15057
  await eom_procedure_codes_tests({ sdk, sdkNonAdmin })
14872
15058
  await cross_org_api_key_tests({ sdk, sdkNonAdmin })
15059
+ await organization_api_keys_tests({ sdk, sdkNonAdmin })
14873
15060
  await organization_settings_duplicates_tests({ sdk, sdkNonAdmin })
14874
15061
  await enduser_session_invalidation_tests({ sdk, sdkNonAdmin })
14875
15062
  await chats_analytics_tests({ sdk, sdkNonAdmin })
Binary file