@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.
- package/lib/cjs/sdk.d.ts +4 -0
- 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/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js +254 -0
- package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/organization_api_keys.test.js +226 -0
- package/lib/cjs/tests/api_tests/organization_api_keys.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +503 -262
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +4 -0
- 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 +1 -0
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js +250 -0
- package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
- package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/organization_api_keys.test.js +222 -0
- package/lib/esm/tests/api_tests/organization_api_keys.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +503 -262
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +11 -0
- package/src/tests/api_tests/organization_api_keys.test.ts +207 -0
- package/src/tests/tests.ts +189 -2
- 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
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -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 })
|
package/test_generated.pdf
CHANGED
|
Binary file
|