@tellescope/sdk 1.254.1 → 1.255.1
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/appointment_no_showed_trigger.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js +236 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js +108 -73
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +587 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.js +165 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.js.map +1 -0
- 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 +560 -305
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +6 -2
- 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/tests/api_tests/appointment_no_showed_trigger.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js +232 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js +109 -74
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +583 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.js +161 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.js.map +1 -0
- 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 +560 -305
- 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/appointment_no_showed_trigger.test.ts +144 -0
- package/src/tests/api_tests/calendar_events_bulk_update.test.ts +22 -0
- package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +381 -0
- package/src/tests/api_tests/organization_api_keys.test.ts +207 -0
- package/src/tests/tests.ts +196 -3
- 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
|
@@ -48,6 +48,8 @@ import { concurrent_build_threads_tests } from "./api_tests/concurrent_build_thr
|
|
|
48
48
|
import { appointment_completed_trigger_tests } from "./api_tests/appointment_completed_trigger.test"
|
|
49
49
|
import { purchase_made_trigger_tests } from "./api_tests/purchase_made_trigger.test"
|
|
50
50
|
import { appointment_rescheduled_trigger_tests } from "./api_tests/appointment_rescheduled_trigger.test"
|
|
51
|
+
import { appointment_no_showed_trigger_tests } from "./api_tests/appointment_no_showed_trigger.test"
|
|
52
|
+
import { group_event_attendee_status_triggers_tests } from "./api_tests/group_event_attendee_status_triggers.test"
|
|
51
53
|
import { journey_error_branching_tests } from "./api_tests/journey_error_branching.test"
|
|
52
54
|
import { afteraction_day_of_month_delay_tests } from "./api_tests/afteraction_day_of_month_delay.test"
|
|
53
55
|
import { push_forms_to_portal_group_completion_tests } from "./api_tests/push_forms_to_portal_group_completion.test"
|
|
@@ -80,6 +82,7 @@ import { enduser_login_tests } from "./api_tests/enduser_login.test";
|
|
|
80
82
|
import { enduser_login_rate_limits_tests } from "./api_tests/enduser_login_rate_limits.test";
|
|
81
83
|
import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test";
|
|
82
84
|
import { cross_org_api_key_tests } from "./api_tests/cross_org_api_key.test";
|
|
85
|
+
import { organization_api_keys_tests } from "./api_tests/organization_api_keys.test";
|
|
83
86
|
import { custom_dashboards_tests } from "./api_tests/custom_dashboards.test";
|
|
84
87
|
import { message_assignment_trigger_tests } from "./api_tests/message_assignment_trigger.test";
|
|
85
88
|
import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent_trigger.test";
|
|
@@ -3493,6 +3496,39 @@ const order_status_equals_tests = async () => {
|
|
|
3493
3496
|
title: "Protocol Condition"
|
|
3494
3497
|
})
|
|
3495
3498
|
|
|
3499
|
+
// Frequency filter triggers (exact-match array filter, mirrors fills/skus). All use a dedicated
|
|
3500
|
+
// source/status so their assertions stay isolated from the t1-t9 incremental tag-count checks above.
|
|
3501
|
+
const tf1 = await sdk.api.automation_triggers.createOne({
|
|
3502
|
+
event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus" } }, // no frequencies -> fires for all
|
|
3503
|
+
action: { type: 'Add Tags', info: { tags: ['Freq No Filter'] }},
|
|
3504
|
+
status: 'Active',
|
|
3505
|
+
title: "Frequency - no filter (backward compat)"
|
|
3506
|
+
})
|
|
3507
|
+
const tf2 = await sdk.api.automation_triggers.createOne({
|
|
3508
|
+
event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['4', '12'] } },
|
|
3509
|
+
action: { type: 'Add Tags', info: { tags: ['Freq Match'] }},
|
|
3510
|
+
status: 'Active',
|
|
3511
|
+
title: "Frequency - multi-value match"
|
|
3512
|
+
})
|
|
3513
|
+
const tf3 = await sdk.api.automation_triggers.createOne({
|
|
3514
|
+
event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: [] } }, // empty array -> fires for all
|
|
3515
|
+
action: { type: 'Add Tags', info: { tags: ['Freq Empty All'] }},
|
|
3516
|
+
status: 'Active',
|
|
3517
|
+
title: "Frequency - empty array means all"
|
|
3518
|
+
})
|
|
3519
|
+
const tf4 = await sdk.api.automation_triggers.createOne({
|
|
3520
|
+
event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqShipped", frequencies: ['4'] } },
|
|
3521
|
+
action: { type: 'Add Tags', info: { tags: ['Freq Status Combo'] }},
|
|
3522
|
+
status: 'Active',
|
|
3523
|
+
title: "Frequency + status combination (AND semantics)"
|
|
3524
|
+
})
|
|
3525
|
+
const tf5 = await sdk.api.automation_triggers.createOne({
|
|
3526
|
+
event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['12'] } },
|
|
3527
|
+
action: { type: 'Add Tags', info: { tags: ['Freq Match 12'] }},
|
|
3528
|
+
status: 'Active',
|
|
3529
|
+
title: "Frequency - second listed value"
|
|
3530
|
+
})
|
|
3531
|
+
|
|
3496
3532
|
const e = await sdk.api.endusers.createOne({})
|
|
3497
3533
|
|
|
3498
3534
|
await sdk.api.enduser_orders.createOne({ status: 'Nooo', source: 'Source', title: 'nomatch', externalId: '1', enduserId: e.id })
|
|
@@ -3705,6 +3741,85 @@ const order_status_equals_tests = async () => {
|
|
|
3705
3741
|
) }
|
|
3706
3742
|
)
|
|
3707
3743
|
|
|
3744
|
+
// --- Frequency filter coverage (fresh enduser keeps tag-count assertions isolated) ---
|
|
3745
|
+
const ef = await sdk.api.endusers.createOne({})
|
|
3746
|
+
|
|
3747
|
+
// Order with NO frequency: no-filter trigger (tf1) and empty-array trigger (tf3) both fire;
|
|
3748
|
+
// populated-filter trigger (tf2) does NOT (missing frequency must never match a populated filter).
|
|
3749
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f1', enduserId: ef.id, title: "Freq No Frequency" })
|
|
3750
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3751
|
+
await async_test(
|
|
3752
|
+
"Frequency: no-filter + empty-array fire for order with no frequency (backward compat)",
|
|
3753
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3754
|
+
{ onResult: e => !!(
|
|
3755
|
+
e.tags?.length === 2
|
|
3756
|
+
&& e.tags?.includes('Freq No Filter')
|
|
3757
|
+
&& e.tags?.includes('Freq Empty All')
|
|
3758
|
+
&& !e.tags?.includes('Freq Match')
|
|
3759
|
+
) }
|
|
3760
|
+
)
|
|
3761
|
+
|
|
3762
|
+
// Order with a frequency NOT in ['4','12'] -> populated-filter trigger still does not fire.
|
|
3763
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f2', enduserId: ef.id, title: "Freq 8", frequency: '8' })
|
|
3764
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3765
|
+
await async_test(
|
|
3766
|
+
"Frequency: no match (order frequency '8' not in ['4','12'])",
|
|
3767
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3768
|
+
{ onResult: e => !!(
|
|
3769
|
+
e.tags?.length === 2
|
|
3770
|
+
&& !e.tags?.includes('Freq Match')
|
|
3771
|
+
) }
|
|
3772
|
+
)
|
|
3773
|
+
|
|
3774
|
+
// Order with frequency '4' -> ['4','12'] trigger fires.
|
|
3775
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f3', enduserId: ef.id, title: "Freq 4", frequency: '4' })
|
|
3776
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3777
|
+
await async_test(
|
|
3778
|
+
"Frequency: match (order frequency '4' is in ['4','12'])",
|
|
3779
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3780
|
+
{ onResult: e => !!(
|
|
3781
|
+
e.tags?.length === 3
|
|
3782
|
+
&& e.tags?.includes('Freq Match')
|
|
3783
|
+
&& !e.tags?.includes('Freq Match 12')
|
|
3784
|
+
) }
|
|
3785
|
+
)
|
|
3786
|
+
|
|
3787
|
+
// Order with frequency '12' -> the second listed value also matches (tf5 frequencies: ['12']).
|
|
3788
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f4', enduserId: ef.id, title: "Freq 12", frequency: '12' })
|
|
3789
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3790
|
+
await async_test(
|
|
3791
|
+
"Frequency: second listed value '12' also matches",
|
|
3792
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3793
|
+
{ onResult: e => !!(
|
|
3794
|
+
e.tags?.length === 4
|
|
3795
|
+
&& e.tags?.includes('Freq Match 12')
|
|
3796
|
+
) }
|
|
3797
|
+
)
|
|
3798
|
+
|
|
3799
|
+
// AND semantics: frequency + status must BOTH match. Right frequency, wrong status -> no fire.
|
|
3800
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f5', enduserId: ef.id, title: "Freq Combo Wrong Freq", frequency: '8' })
|
|
3801
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3802
|
+
await async_test(
|
|
3803
|
+
"Frequency + status combo: status matches but frequency does not -> no fire",
|
|
3804
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3805
|
+
{ onResult: e => !!(
|
|
3806
|
+
e.tags?.length === 4
|
|
3807
|
+
&& !e.tags?.includes('Freq Status Combo')
|
|
3808
|
+
) }
|
|
3809
|
+
)
|
|
3810
|
+
|
|
3811
|
+
// AND semantics: right status AND right frequency -> fires.
|
|
3812
|
+
await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f6', enduserId: ef.id, title: "Freq Combo Match", frequency: '4' })
|
|
3813
|
+
await wait(undefined, 500) // allow triggers to happen
|
|
3814
|
+
await async_test(
|
|
3815
|
+
"Frequency + status combo: both match -> fires (AND semantics)",
|
|
3816
|
+
() => sdk.api.endusers.getOne(ef.id),
|
|
3817
|
+
{ onResult: e => !!(
|
|
3818
|
+
e.tags?.length === 5
|
|
3819
|
+
&& e.tags?.includes('Freq Status Combo')
|
|
3820
|
+
) }
|
|
3821
|
+
)
|
|
3822
|
+
|
|
3708
3823
|
await Promise.all([
|
|
3709
3824
|
sdk.api.automation_triggers.deleteOne(t1.id),
|
|
3710
3825
|
sdk.api.automation_triggers.deleteOne(t2.id),
|
|
@@ -3715,7 +3830,13 @@ const order_status_equals_tests = async () => {
|
|
|
3715
3830
|
sdk.api.automation_triggers.deleteOne(t7.id),
|
|
3716
3831
|
sdk.api.automation_triggers.deleteOne(t8.id),
|
|
3717
3832
|
sdk.api.automation_triggers.deleteOne(t9.id),
|
|
3833
|
+
sdk.api.automation_triggers.deleteOne(tf1.id),
|
|
3834
|
+
sdk.api.automation_triggers.deleteOne(tf2.id),
|
|
3835
|
+
sdk.api.automation_triggers.deleteOne(tf3.id),
|
|
3836
|
+
sdk.api.automation_triggers.deleteOne(tf4.id),
|
|
3837
|
+
sdk.api.automation_triggers.deleteOne(tf5.id),
|
|
3718
3838
|
sdk.api.endusers.deleteOne(e.id),
|
|
3839
|
+
sdk.api.endusers.deleteOne(ef.id),
|
|
3719
3840
|
])
|
|
3720
3841
|
}
|
|
3721
3842
|
|
|
@@ -5592,6 +5713,8 @@ const trigger_events_api_tests = async () => {
|
|
|
5592
5713
|
const automation_trigger_tests = async () => {
|
|
5593
5714
|
log_header("Automation Trigger Tests")
|
|
5594
5715
|
|
|
5716
|
+
await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
|
|
5717
|
+
await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
|
|
5595
5718
|
await assign_care_team_tests()
|
|
5596
5719
|
await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
|
|
5597
5720
|
await order_status_equals_tests()
|
|
@@ -6717,11 +6840,25 @@ export const self_serve_appointment_booking_tests = async () => {
|
|
|
6717
6840
|
confirmationEmailDisabled: true,
|
|
6718
6841
|
confirmationSMSDisabled: true,
|
|
6719
6842
|
})
|
|
6720
|
-
const event30minGroup = await sdk.api.calendar_event_templates.createOne({
|
|
6843
|
+
const event30minGroup = await sdk.api.calendar_event_templates.createOne({
|
|
6721
6844
|
title: 'test group', durationInMinutes: 30,
|
|
6722
6845
|
confirmationEmailDisabled: true,
|
|
6723
6846
|
confirmationSMSDisabled: true,
|
|
6724
|
-
enduserAttendeeLimit: 2,
|
|
6847
|
+
enduserAttendeeLimit: 2,
|
|
6848
|
+
})
|
|
6849
|
+
const event30minTwilio = await sdk.api.calendar_event_templates.createOne({
|
|
6850
|
+
title: 'test twilio', durationInMinutes: 30,
|
|
6851
|
+
confirmationEmailDisabled: true,
|
|
6852
|
+
confirmationSMSDisabled: true,
|
|
6853
|
+
enableVideoCall: true,
|
|
6854
|
+
videoIntegration: 'Twilio',
|
|
6855
|
+
})
|
|
6856
|
+
const event30minZoom = await sdk.api.calendar_event_templates.createOne({
|
|
6857
|
+
title: 'test zoom', durationInMinutes: 30,
|
|
6858
|
+
confirmationEmailDisabled: true,
|
|
6859
|
+
confirmationSMSDisabled: true,
|
|
6860
|
+
enableVideoCall: true,
|
|
6861
|
+
videoIntegration: 'Zoom',
|
|
6725
6862
|
})
|
|
6726
6863
|
|
|
6727
6864
|
// ensure it doesn't match current day, to avoid errors on testing
|
|
@@ -7281,17 +7418,70 @@ export const self_serve_appointment_booking_tests = async () => {
|
|
|
7281
7418
|
{ onResult: event => !!event.rescheduledAt }
|
|
7282
7419
|
)
|
|
7283
7420
|
|
|
7421
|
+
// videoIntegration passthrough from template to booked event (Twilio fix + Zoom regression guard)
|
|
7422
|
+
// set up fresh, dedicated availability so this doesn't interfere with the slot-accounting tests above
|
|
7423
|
+
await sdk.api.users.updateOne(sdk.userInfo.id, {
|
|
7424
|
+
weeklyAvailabilities: [
|
|
7425
|
+
{
|
|
7426
|
+
dayOfWeekStartingSundayIndexedByZero,
|
|
7427
|
+
startTimeInMinutes: 60 * 9, // 9am,
|
|
7428
|
+
endTimeInMinutes: 60 * 11, // 11am,
|
|
7429
|
+
},
|
|
7430
|
+
],
|
|
7431
|
+
timezone: 'America/New_York',
|
|
7432
|
+
}, {
|
|
7433
|
+
replaceObjectFields: true,
|
|
7434
|
+
})
|
|
7435
|
+
|
|
7436
|
+
const twilioSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
|
|
7437
|
+
calendarEventTemplateId: event30minTwilio.id,
|
|
7438
|
+
from: new Date(Date.now() - 10000),
|
|
7439
|
+
to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
|
|
7440
|
+
userIds: [sdk.userInfo.id],
|
|
7441
|
+
})
|
|
7442
|
+
const bookedTwilioAppointment = (await enduserSDK.api.calendar_events.book_appointment({
|
|
7443
|
+
calendarEventTemplateId: event30minTwilio.id,
|
|
7444
|
+
startTime: new Date(twilioSlots.availabilityBlocks[0].startTimeInMS),
|
|
7445
|
+
userId: sdk.userInfo.id,
|
|
7446
|
+
})).createdEvent
|
|
7447
|
+
assert(
|
|
7448
|
+
bookedTwilioAppointment.videoIntegration === 'Twilio',
|
|
7449
|
+
'Twilio videoIntegration not propagated to booked event',
|
|
7450
|
+
'Twilio videoIntegration propagated to booked event'
|
|
7451
|
+
)
|
|
7452
|
+
|
|
7453
|
+
const zoomSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
|
|
7454
|
+
calendarEventTemplateId: event30minZoom.id,
|
|
7455
|
+
from: new Date(Date.now() - 10000),
|
|
7456
|
+
to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
|
|
7457
|
+
userIds: [sdk.userInfo.id],
|
|
7458
|
+
})
|
|
7459
|
+
const bookedZoomAppointment = (await enduserSDK.api.calendar_events.book_appointment({
|
|
7460
|
+
calendarEventTemplateId: event30minZoom.id,
|
|
7461
|
+
startTime: new Date(zoomSlots.availabilityBlocks[0].startTimeInMS),
|
|
7462
|
+
userId: sdk.userInfo.id,
|
|
7463
|
+
})).createdEvent
|
|
7464
|
+
assert(
|
|
7465
|
+
bookedZoomAppointment.videoIntegration === 'Zoom',
|
|
7466
|
+
'Zoom videoIntegration not propagated to booked event',
|
|
7467
|
+
'Zoom videoIntegration propagated to booked event'
|
|
7468
|
+
)
|
|
7469
|
+
|
|
7284
7470
|
await Promise.all([
|
|
7285
7471
|
sdk.api.endusers.deleteOne(e1.id),
|
|
7286
7472
|
sdk.api.endusers.deleteOne(e2.id),
|
|
7287
7473
|
sdk.api.endusers.deleteOne(e3.id),
|
|
7288
7474
|
sdk.api.calendar_event_templates.deleteOne(event30min.id),
|
|
7289
7475
|
sdk.api.calendar_event_templates.deleteOne(event30minGroup.id),
|
|
7476
|
+
sdk.api.calendar_event_templates.deleteOne(event30minTwilio.id),
|
|
7477
|
+
sdk.api.calendar_event_templates.deleteOne(event30minZoom.id),
|
|
7290
7478
|
sdk.api.calendar_event_templates.deleteOne(event15min.id),
|
|
7291
7479
|
sdk.api.calendar_event_templates.deleteOne(templatePreserveHosts.id),
|
|
7292
7480
|
sdk.api.calendar_event_templates.deleteOne(templateReplaceHosts.id),
|
|
7293
7481
|
sdk.api.calendar_events.deleteOne(bookedAppointment.id),
|
|
7294
7482
|
sdk.api.calendar_events.deleteOne(bookedMultiAppointment.id),
|
|
7483
|
+
sdk.api.calendar_events.deleteOne(bookedTwilioAppointment.id),
|
|
7484
|
+
sdk.api.calendar_events.deleteOne(bookedZoomAppointment.id),
|
|
7295
7485
|
sdk.api.calendar_events.deleteOne(groupEvent.id),
|
|
7296
7486
|
sdk.api.calendar_events.deleteOne(originalMultiHostAppt.id),
|
|
7297
7487
|
sdk.api.calendar_events.deleteOne(rescheduledPreserveHosts.id),
|
|
@@ -11237,6 +11427,7 @@ const lockout_tests = async () => {
|
|
|
11237
11427
|
() => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
|
|
11238
11428
|
passOnAnyResult
|
|
11239
11429
|
)
|
|
11430
|
+
await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
|
|
11240
11431
|
await async_test(
|
|
11241
11432
|
"non-admin can re authenciate when locked to 0",
|
|
11242
11433
|
() => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
|
|
@@ -11269,6 +11460,7 @@ const lockout_tests = async () => {
|
|
|
11269
11460
|
() => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
|
|
11270
11461
|
passOnAnyResult
|
|
11271
11462
|
)
|
|
11463
|
+
await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
|
|
11272
11464
|
await async_test(
|
|
11273
11465
|
"non-admin can re authenciate when locked to future date",
|
|
11274
11466
|
() => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
|
|
@@ -14842,6 +15034,7 @@ const ip_address_form_tests = async () => {
|
|
|
14842
15034
|
await replace_form_field_template_values_tests()
|
|
14843
15035
|
await mfa_tests()
|
|
14844
15036
|
await setup_tests(sdk, sdkNonAdmin)
|
|
15037
|
+
await automation_trigger_tests()
|
|
14845
15038
|
await resource_access_tags_tests({ sdk, sdkNonAdmin })
|
|
14846
15039
|
await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
|
|
14847
15040
|
await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
|
|
@@ -14852,7 +15045,6 @@ const ip_address_form_tests = async () => {
|
|
|
14852
15045
|
await load_inbox_redaction_tests({ sdk, sdkNonAdmin })
|
|
14853
15046
|
await webhook_timeout_tests({ sdk, sdkNonAdmin })
|
|
14854
15047
|
await user_portal_settings_tests({ sdk, sdkNonAdmin })
|
|
14855
|
-
await automation_trigger_tests()
|
|
14856
15048
|
await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
|
|
14857
15049
|
await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
|
|
14858
15050
|
await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
|
|
@@ -14870,6 +15062,7 @@ const ip_address_form_tests = async () => {
|
|
|
14870
15062
|
await enduser_cross_access_isolation_tests({ sdk, sdkNonAdmin })
|
|
14871
15063
|
await eom_procedure_codes_tests({ sdk, sdkNonAdmin })
|
|
14872
15064
|
await cross_org_api_key_tests({ sdk, sdkNonAdmin })
|
|
15065
|
+
await organization_api_keys_tests({ sdk, sdkNonAdmin })
|
|
14873
15066
|
await organization_settings_duplicates_tests({ sdk, sdkNonAdmin })
|
|
14874
15067
|
await enduser_session_invalidation_tests({ sdk, sdkNonAdmin })
|
|
14875
15068
|
await chats_analytics_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|