@tellescope/sdk 1.256.0 → 1.256.2
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 +2 -2
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.js +163 -0
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js +224 -0
- package/lib/cjs/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/embeddables_auth_token.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/embeddables_auth_token.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/embeddables_auth_token.test.js +354 -0
- package/lib/cjs/tests/api_tests/embeddables_auth_token.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.js +351 -0
- package/lib/cjs/tests/api_tests/start_ai_conversation.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +217 -189
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/phone_validation.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/phone_validation.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/phone_validation.test.js +96 -0
- package/lib/cjs/tests/unit_tests/phone_validation.test.js.map +1 -0
- package/lib/esm/sdk.d.ts +2 -2
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.js +156 -0
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.js.map +1 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js +220 -0
- package/lib/esm/tests/api_tests/beluga_trigger_refill.test.js.map +1 -0
- package/lib/esm/tests/api_tests/embeddables_auth_token.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/embeddables_auth_token.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/embeddables_auth_token.test.js +347 -0
- package/lib/esm/tests/api_tests/embeddables_auth_token.test.js.map +1 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.js +347 -0
- package/lib/esm/tests/api_tests/start_ai_conversation.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +217 -189
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/phone_validation.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/phone_validation.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/phone_validation.test.js +93 -0
- package/lib/esm/tests/unit_tests/phone_validation.test.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/appointment_link_expiration.test.ts +92 -0
- package/src/tests/api_tests/beluga_trigger_refill.test.ts +152 -0
- package/src/tests/api_tests/embeddables_auth_token.test.ts +417 -0
- package/src/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.ts +292 -0
- package/src/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.ts +179 -0
- package/src/tests/api_tests/start_ai_conversation.test.ts +256 -0
- package/src/tests/tests.ts +14 -0
- package/src/tests/unit_tests/phone_validation.test.ts +108 -0
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import { log_header, wait, async_test, assert } from "@tellescope/testing"
|
|
5
|
+
import { setup_tests } from "../setup"
|
|
6
|
+
|
|
7
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
8
|
+
|
|
9
|
+
const TEST_MARKER = '__TELLESCOPE_TEST_INCOMING_SMS_AUTOMATION__'
|
|
10
|
+
const TEST_PHONE = '+15555555555' // send_sms short-circuits real sends for this number
|
|
11
|
+
|
|
12
|
+
// SMS AI conversations (startAIConversation journey action).
|
|
13
|
+
// Runtime flows (start/supersede/takeover/inbound absorption) require the organization to have an
|
|
14
|
+
// SMS number configured — when it doesn't (typical dev org), those sections assert the
|
|
15
|
+
// configuration error path and are otherwise skipped; config validation runs everywhere.
|
|
16
|
+
export const start_ai_conversation_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
17
|
+
log_header("Start AI Conversation (SMS Agent) Tests")
|
|
18
|
+
|
|
19
|
+
const users = await sdk.api.users.getSome({})
|
|
20
|
+
const senderId = users[0].id
|
|
21
|
+
|
|
22
|
+
const journey = await sdk.api.journeys.createOne({ title: 'SMS Agent Test Journey' })
|
|
23
|
+
const createdEnduserIds: string[] = []
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
/*************** Config validation round-trip ***************/
|
|
27
|
+
const validInfo = {
|
|
28
|
+
channel: 'SMS' as const,
|
|
29
|
+
senderId,
|
|
30
|
+
initialMessage: 'Hello {{enduser.fname}}! Quick question for you.',
|
|
31
|
+
prompt: 'You are texting on behalf of Test Clinic. Answer briefly.',
|
|
32
|
+
outcomes: [{ value: 'resolved', description: 'The question was answered' }],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const step = await sdk.api.automation_steps.createOne({
|
|
36
|
+
journeyId: journey.id,
|
|
37
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
38
|
+
action: { type: 'startAIConversation', info: validInfo },
|
|
39
|
+
})
|
|
40
|
+
assert(
|
|
41
|
+
step.action.type === 'startAIConversation' && step.action.info.initialMessage === validInfo.initialMessage,
|
|
42
|
+
'startAIConversation step round-trip failed',
|
|
43
|
+
'startAIConversation step round-trips',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const outcomeChild = await sdk.api.automation_steps.createOne({
|
|
47
|
+
journeyId: journey.id,
|
|
48
|
+
events: [{ type: 'onAIConversationOutcome', info: { automationStepId: step.id, outcome: 'resolved' } }],
|
|
49
|
+
action: { type: 'addEnduserTags', info: { tags: ['sms-agent-resolved'] } },
|
|
50
|
+
})
|
|
51
|
+
assert(
|
|
52
|
+
outcomeChild.events[0].type === 'onAIConversationOutcome',
|
|
53
|
+
'onAIConversationOutcome event round-trip failed',
|
|
54
|
+
'onAIConversationOutcome event round-trips',
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
await async_test('startAIConversation without prompt is rejected',
|
|
58
|
+
() => sdk.api.automation_steps.createOne({
|
|
59
|
+
journeyId: journey.id,
|
|
60
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
61
|
+
action: { type: 'startAIConversation', info: { ...validInfo, prompt: undefined } as any },
|
|
62
|
+
}),
|
|
63
|
+
{ shouldError: true, onError: () => true },
|
|
64
|
+
)
|
|
65
|
+
await async_test('startAIConversation without senderId is rejected',
|
|
66
|
+
() => sdk.api.automation_steps.createOne({
|
|
67
|
+
journeyId: journey.id,
|
|
68
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
69
|
+
action: { type: 'startAIConversation', info: { ...validInfo, senderId: undefined } as any },
|
|
70
|
+
}),
|
|
71
|
+
{ shouldError: true, onError: () => true },
|
|
72
|
+
)
|
|
73
|
+
await async_test('startAIConversation without initialMessage is rejected',
|
|
74
|
+
() => sdk.api.automation_steps.createOne({
|
|
75
|
+
journeyId: journey.id,
|
|
76
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
77
|
+
action: { type: 'startAIConversation', info: { ...validInfo, initialMessage: undefined } as any },
|
|
78
|
+
}),
|
|
79
|
+
{ shouldError: true, onError: () => true },
|
|
80
|
+
)
|
|
81
|
+
await async_test('startAIConversation with invalid tool is rejected',
|
|
82
|
+
() => sdk.api.automation_steps.createOne({
|
|
83
|
+
journeyId: journey.id,
|
|
84
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
85
|
+
action: { type: 'startAIConversation', info: { ...validInfo, tools: [{ type: 'Submit Form', info: { formId: 'not-an-id' } }] } as any },
|
|
86
|
+
}),
|
|
87
|
+
{ shouldError: true, onError: () => true },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
/*************** Runtime: the process endpoint ***************/
|
|
91
|
+
const processFor = async (enduserId: string) => {
|
|
92
|
+
const automatedAction = await sdk.api.automated_actions.createOne({
|
|
93
|
+
enduserId,
|
|
94
|
+
journeyId: journey.id,
|
|
95
|
+
automationStepId: step.id,
|
|
96
|
+
event: { type: 'onJourneyStart', info: {} },
|
|
97
|
+
action: { type: 'startAIConversation', info: validInfo },
|
|
98
|
+
status: 'active',
|
|
99
|
+
processAfter: Date.now(),
|
|
100
|
+
})
|
|
101
|
+
return sdk.api.automated_actions.process({ automatedActionId: automatedAction.id })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// no-phone error path works regardless of org SMS configuration
|
|
105
|
+
const phonelessEnduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-nophone' })
|
|
106
|
+
createdEnduserIds.push(phonelessEnduser.id)
|
|
107
|
+
const noPhoneResult = await processFor(phonelessEnduser.id)
|
|
108
|
+
assert(
|
|
109
|
+
!noPhoneResult.success && !!noPhoneResult.error,
|
|
110
|
+
'phoneless start should fail with an error',
|
|
111
|
+
'start action errors for a contact without a phone',
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// resolve the same organization the action does (last organizationId, else businessId) —
|
|
115
|
+
// getSome({})[0] is an arbitrary sub-organization and can be a different record entirely
|
|
116
|
+
const session = sdk.userInfo as { businessId?: string, organizationIds?: string[] }
|
|
117
|
+
const resolvedOrgId = [...(session.organizationIds ?? [])].pop() || session.businessId
|
|
118
|
+
const organization = (await sdk.api.organizations.getSome({})).find(o => o.id === resolvedOrgId)
|
|
119
|
+
const orgHasNumber = !!(organization as any)?.twilioNumber || !!((organization as any)?.twilioNumbers?.length)
|
|
120
|
+
|
|
121
|
+
if (!orgHasNumber) {
|
|
122
|
+
// still assert the wiring: the action processes and reports a configuration error rather than
|
|
123
|
+
// crashing. AI enablement is checked before the SMS number and short-circuits first, and
|
|
124
|
+
// bedrockAIAllowed/creditCount are super-admin-only (unreadable here), so accept either.
|
|
125
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-nonumber', phone: TEST_PHONE })
|
|
126
|
+
createdEnduserIds.push(enduser.id)
|
|
127
|
+
const result = await processFor(enduser.id)
|
|
128
|
+
assert(
|
|
129
|
+
!result.success && /no sms number configured|ai is not enabled/i.test(result.error || ''),
|
|
130
|
+
`expected a configuration error, got: ${result.error}`,
|
|
131
|
+
'start action reports a configuration error (org has no SMS number — runtime flows skipped)',
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*************** Runtime flows (org has an SMS number) ***************/
|
|
137
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-runtime', phone: TEST_PHONE })
|
|
138
|
+
createdEnduserIds.push(enduser.id)
|
|
139
|
+
|
|
140
|
+
const startResult = await processFor(enduser.id)
|
|
141
|
+
assert(startResult.success === true, `start failed: ${startResult.error}`, 'start action succeeds')
|
|
142
|
+
await wait(undefined, 1000)
|
|
143
|
+
|
|
144
|
+
let refreshed = await sdk.api.endusers.getOne(enduser.id)
|
|
145
|
+
const entry = (refreshed.aiConversations ?? []).find(c => c.active)
|
|
146
|
+
assert(!!entry, 'no active aiConversations entry on enduser', 'enduser has an active conversation entry')
|
|
147
|
+
assert(entry?.destination === TEST_PHONE, 'wrong destination on entry', 'conversation entry pairs the contact number')
|
|
148
|
+
|
|
149
|
+
const conversation = await sdk.api.ai_conversations.getOne(entry!.aiConversationId)
|
|
150
|
+
assert(conversation.type === 'sms_agent', 'wrong ai_conversations type', 'ai_conversations doc has type sms_agent')
|
|
151
|
+
|
|
152
|
+
const initialMessages = await sdk.api.sms_messages.getSome({ filter: { enduserId: enduser.id } })
|
|
153
|
+
const outbound = initialMessages.find(m => !m.inbound && m.automationStepId === step.id)
|
|
154
|
+
// exact equality on a merge-field-containing opener guards two regressions: templating must
|
|
155
|
+
// happen in send_sms (raw text on the record), and the agent-reply {{ escaping must NOT be
|
|
156
|
+
// applied to the org-authored initial message (preserveMergeFields)
|
|
157
|
+
assert(
|
|
158
|
+
!!outbound && outbound.message === validInfo.initialMessage,
|
|
159
|
+
'initial SMS record missing or not raw text',
|
|
160
|
+
'initial SMS logged with raw (untemplated) text + automationStepId',
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// supersede: second start on the same pairing replaces the first
|
|
164
|
+
const secondStart = await processFor(enduser.id)
|
|
165
|
+
assert(secondStart.success === true, `second start failed: ${secondStart.error}`, 'duplicate start succeeds (replace)')
|
|
166
|
+
await wait(undefined, 1000)
|
|
167
|
+
refreshed = await sdk.api.endusers.getOne(enduser.id)
|
|
168
|
+
const activeEntries = (refreshed.aiConversations ?? []).filter(c => c.active)
|
|
169
|
+
assert(activeEntries.length === 1, 'expected exactly one active conversation', 'supersede leaves exactly one active conversation')
|
|
170
|
+
assert(
|
|
171
|
+
activeEntries[0].aiConversationId !== entry!.aiConversationId,
|
|
172
|
+
'active conversation was not replaced',
|
|
173
|
+
'supersede started a fresh conversation',
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// inbound absorption via the testing hook: STOP ends the conversation
|
|
177
|
+
await sdk.api.sms_messages.createOne({
|
|
178
|
+
enduserId: enduser.id,
|
|
179
|
+
message: `stop${TEST_MARKER}`, // marker is stripped before the AI branch sees the text
|
|
180
|
+
inbound: true,
|
|
181
|
+
logOnly: true,
|
|
182
|
+
phoneNumber: activeEntries[0].source,
|
|
183
|
+
enduserPhoneNumber: TEST_PHONE,
|
|
184
|
+
} as any)
|
|
185
|
+
await wait(undefined, 2000)
|
|
186
|
+
refreshed = await sdk.api.endusers.getOne(enduser.id)
|
|
187
|
+
const stopped = (refreshed.aiConversations ?? []).find(c => c.aiConversationId === activeEntries[0].aiConversationId)
|
|
188
|
+
assert(
|
|
189
|
+
stopped?.active === false && stopped?.endedReason === 'unsubscribed',
|
|
190
|
+
`STOP did not end the conversation (${JSON.stringify(stopped)})`,
|
|
191
|
+
'STOP reply ends the conversation',
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// staff takeover: start again, then a manual (user-session) SMS on the thread ends it
|
|
195
|
+
const takeoverStart = await processFor(enduser.id)
|
|
196
|
+
assert(takeoverStart.success === true, `takeover-start failed: ${takeoverStart.error}`, 'start (for takeover test) succeeds')
|
|
197
|
+
await wait(undefined, 1000)
|
|
198
|
+
await sdk.api.sms_messages.createOne({
|
|
199
|
+
enduserId: enduser.id,
|
|
200
|
+
message: 'Hi, this is a real person taking over.',
|
|
201
|
+
inbound: false,
|
|
202
|
+
phoneNumber: (await sdk.api.endusers.getOne(enduser.id)).aiConversations?.find(c => c.active)?.source,
|
|
203
|
+
enduserPhoneNumber: TEST_PHONE,
|
|
204
|
+
} as any)
|
|
205
|
+
await wait(undefined, 2000)
|
|
206
|
+
refreshed = await sdk.api.endusers.getOne(enduser.id)
|
|
207
|
+
const takenOver = (refreshed.aiConversations ?? []).find(c => c.endedReason === 'staff-takeover')
|
|
208
|
+
assert(!!takenOver, 'no staff-takeover ended entry found', 'manual staff SMS ends the conversation (takeover)')
|
|
209
|
+
|
|
210
|
+
// no-conversation baseline: marker inbound for a fresh enduser produces zero AI artifacts
|
|
211
|
+
const baselineEnduser = await sdk.api.endusers.createOne({ fname: 'sms-agent-baseline', phone: '+15555555554' })
|
|
212
|
+
createdEnduserIds.push(baselineEnduser.id)
|
|
213
|
+
await sdk.api.sms_messages.createOne({
|
|
214
|
+
enduserId: baselineEnduser.id,
|
|
215
|
+
message: `hello there${TEST_MARKER}`,
|
|
216
|
+
inbound: true,
|
|
217
|
+
logOnly: true,
|
|
218
|
+
phoneNumber: activeEntries[0].source,
|
|
219
|
+
enduserPhoneNumber: '+15555555554',
|
|
220
|
+
} as any)
|
|
221
|
+
await wait(undefined, 2000)
|
|
222
|
+
const baselineRefreshed = await sdk.api.endusers.getOne(baselineEnduser.id)
|
|
223
|
+
const baselineMessages = await sdk.api.sms_messages.getSome({ filter: { enduserId: baselineEnduser.id } })
|
|
224
|
+
assert(
|
|
225
|
+
!(baselineRefreshed.aiConversations ?? []).length && !baselineMessages.find(m => !m.inbound),
|
|
226
|
+
'AI artifacts appeared for an enduser with no conversation',
|
|
227
|
+
'no-conversation inbound produces zero AI artifacts (regression baseline)',
|
|
228
|
+
)
|
|
229
|
+
} finally {
|
|
230
|
+
await sdk.api.journeys.deleteOne(journey.id).catch(() => {})
|
|
231
|
+
for (const id of createdEnduserIds) {
|
|
232
|
+
await sdk.api.endusers.deleteOne(id).catch(() => {})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Allow running this test file independently
|
|
238
|
+
if (require.main === module) {
|
|
239
|
+
const sdk = new Session({ host })
|
|
240
|
+
const sdkNonAdmin = new Session({ host })
|
|
241
|
+
|
|
242
|
+
const runTests = async () => {
|
|
243
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
244
|
+
await start_ai_conversation_tests({ sdk, sdkNonAdmin })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
runTests()
|
|
248
|
+
.then(() => {
|
|
249
|
+
console.log("✅ Start AI Conversation test suite completed successfully")
|
|
250
|
+
process.exit(0)
|
|
251
|
+
})
|
|
252
|
+
.catch((error) => {
|
|
253
|
+
console.error("❌ Start AI Conversation test suite failed:", error)
|
|
254
|
+
process.exit(1)
|
|
255
|
+
})
|
|
256
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { Session, APIQuery, EnduserSession } from "../sdk"
|
|
39
39
|
import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_observations_acknowledge.test"
|
|
40
40
|
import { translations_tests } from "./api_tests/translations.test"
|
|
41
|
+
import { embeddables_auth_token_tests } from "./api_tests/embeddables_auth_token.test"
|
|
41
42
|
import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
|
|
42
43
|
import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
|
|
43
44
|
import { healthie_multi_integration_tests } from "./api_tests/healthie_multi_integration.test"
|
|
@@ -97,6 +98,7 @@ import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent
|
|
|
97
98
|
import { time_tracks_tests, time_tracks_historical_tests, time_tracks_correction_tests, time_tracks_review_tests, time_tracks_lock_tests, time_tracks_edge_case_tests, time_tracks_resubmit_tests, time_tracks_appointment_duration_tests } from "./api_tests/time_tracks.test";
|
|
98
99
|
import { monthly_availability_restrictions_tests } from "./api_tests/monthly_availability_restrictions.test";
|
|
99
100
|
import { calendar_event_limits_tests } from "./api_tests/calendar_event_limits.test";
|
|
101
|
+
import { appointment_link_expiration_tests } from "./api_tests/appointment_link_expiration.test";
|
|
100
102
|
import { custom_aggregation_tests } from "./api_tests/custom_aggregation.test";
|
|
101
103
|
import { chats_analytics_tests } from "./api_tests/chats_analytics.test";
|
|
102
104
|
import { nutrition_tracking_tests } from "./api_tests/nutrition_tracking.test";
|
|
@@ -120,6 +122,8 @@ import { load_inbox_redaction_tests } from "./api_tests/security/F-0151-load-inb
|
|
|
120
122
|
import { webhook_timeout_tests } from "./api_tests/security/F-0155-webhook-timeout.test";
|
|
121
123
|
import { enduser_ai_summary_trust_gate_tests } from "./api_tests/security/enduser-ai-summary-trust-gate.test";
|
|
122
124
|
import { allowed_paths_confinement_tests } from "./api_tests/security/F-0166-allowed-paths-wildcard-bypass.test";
|
|
125
|
+
import { proxy_image_ssrf_tests } from "./api_tests/security/F-0025-proxy-image-unauth-ssrf.test";
|
|
126
|
+
import { webhook_outbound_ssrf_tests } from "./api_tests/security/F-0015-webhook-outbound-ssrf.test";
|
|
123
127
|
import { public_endpoint_auth_field_lock_tests } from "./api_tests/security/public_endpoint_auth_field_lock.test";
|
|
124
128
|
import { formsort_webhook_auth_field_lock_tests } from "./api_tests/security/formsort_webhook_auth_field_lock.test";
|
|
125
129
|
import { bulk_assignment_tests } from "./api_tests/bulk_assignment.test";
|
|
@@ -133,9 +137,11 @@ import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
|
|
|
133
137
|
import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
|
|
134
138
|
import { formsort_header_auth_tests } from "./api_tests/formsort_header_auth.test";
|
|
135
139
|
import { form_submitted_trigger_tests } from "./api_tests/form_submitted_trigger.test";
|
|
140
|
+
import { start_ai_conversation_tests } from "./api_tests/start_ai_conversation.test";
|
|
136
141
|
import { dont_sync_to_elation_form_submission_tests } from "./api_tests/dont_sync_to_elation_form_submission.test";
|
|
137
142
|
import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
|
|
138
143
|
import { conditional_logic_medication_unit_tests } from "./unit_tests/conditional_logic_medication.test";
|
|
144
|
+
import { phone_validation_unit_tests } from "./unit_tests/phone_validation.test";
|
|
139
145
|
import { elation_user_id_tests } from "./api_tests/elation_user_id.test";
|
|
140
146
|
import { organization_settings_duplicates_tests } from "./api_tests/organization_settings_duplicates.test";
|
|
141
147
|
import { calendar_events_bulk_update_tests } from "./api_tests/calendar_events_bulk_update.test";
|
|
@@ -144,6 +150,7 @@ import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
|
|
|
144
150
|
import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
|
|
145
151
|
import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
|
|
146
152
|
import { beluga_manual_sync_tests } from "./api_tests/beluga_manual_sync.test";
|
|
153
|
+
import { beluga_trigger_refill_tests } from "./api_tests/beluga_trigger_refill.test";
|
|
147
154
|
import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
|
|
148
155
|
import { account_switcher_tests } from "./api_tests/account_switcher.test";
|
|
149
156
|
import { totp_mfa_tests } from "./api_tests/totp_mfa.test";
|
|
@@ -15270,10 +15277,14 @@ const ip_address_form_tests = async () => {
|
|
|
15270
15277
|
|
|
15271
15278
|
await enduser_conditional_logic_tests()
|
|
15272
15279
|
await conditional_logic_medication_unit_tests()
|
|
15280
|
+
await phone_validation_unit_tests()
|
|
15273
15281
|
await replace_enduser_template_values_tests()
|
|
15274
15282
|
await replace_form_field_template_values_tests()
|
|
15275
15283
|
await mfa_tests()
|
|
15276
15284
|
await setup_tests(sdk, sdkNonAdmin)
|
|
15285
|
+
await embeddables_auth_token_tests({ sdk, sdkNonAdmin })
|
|
15286
|
+
await proxy_image_ssrf_tests({ sdk, sdkNonAdmin })
|
|
15287
|
+
await webhook_outbound_ssrf_tests({ sdk, sdkNonAdmin })
|
|
15277
15288
|
await allowed_paths_confinement_tests({ sdk, sdkNonAdmin })
|
|
15278
15289
|
await phone_calls_conference_hold_tests({ sdk, sdkNonAdmin })
|
|
15279
15290
|
await tickets_bulk_assign_care_team_tests({ sdk, sdkNonAdmin })
|
|
@@ -15282,6 +15293,7 @@ const ip_address_form_tests = async () => {
|
|
|
15282
15293
|
await automation_trigger_tests()
|
|
15283
15294
|
await resource_access_tags_tests({ sdk, sdkNonAdmin })
|
|
15284
15295
|
await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
|
|
15296
|
+
await beluga_trigger_refill_tests({ sdk, sdkNonAdmin })
|
|
15285
15297
|
await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
|
|
15286
15298
|
await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
|
|
15287
15299
|
await enduser_write_restrictions_tests({ sdk, sdkNonAdmin })
|
|
@@ -15320,6 +15332,7 @@ const ip_address_form_tests = async () => {
|
|
|
15320
15332
|
await chats_analytics_tests({ sdk, sdkNonAdmin })
|
|
15321
15333
|
await field_redaction_tests({ sdk, sdkNonAdmin })
|
|
15322
15334
|
await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
|
|
15335
|
+
await start_ai_conversation_tests({ sdk, sdkNonAdmin })
|
|
15323
15336
|
await dont_sync_to_elation_form_submission_tests({ sdk, sdkNonAdmin })
|
|
15324
15337
|
await date_string_validation_tests({ sdk, sdkNonAdmin })
|
|
15325
15338
|
await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
|
|
@@ -15338,6 +15351,7 @@ const ip_address_form_tests = async () => {
|
|
|
15338
15351
|
await time_tracks_resubmit_tests({ sdk, sdkNonAdmin })
|
|
15339
15352
|
await time_tracks_appointment_duration_tests({ sdk, sdkNonAdmin })
|
|
15340
15353
|
await calendar_event_limits_tests({ sdk, sdkNonAdmin })
|
|
15354
|
+
await appointment_link_expiration_tests({ sdk, sdkNonAdmin })
|
|
15341
15355
|
await get_some_projection_tests({ sdk, sdkNonAdmin })
|
|
15342
15356
|
await file_download_unicode_names_tests({ sdk, sdkNonAdmin })
|
|
15343
15357
|
await elation_user_id_tests({ sdk, sdkNonAdmin })
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parse_phone_number,
|
|
3
|
+
phoneValidator,
|
|
4
|
+
phoneValidatorOptional,
|
|
5
|
+
} from "@tellescope/validation"
|
|
6
|
+
import { to_human_readable_phone_number } from "@tellescope/utilities"
|
|
7
|
+
|
|
8
|
+
let failures = 0
|
|
9
|
+
let passed = 0
|
|
10
|
+
|
|
11
|
+
const assert = (name: string, actual: unknown, expected: unknown) => {
|
|
12
|
+
if (actual === expected) {
|
|
13
|
+
passed++
|
|
14
|
+
console.log(` ✓ ${name}`)
|
|
15
|
+
} else {
|
|
16
|
+
failures++
|
|
17
|
+
console.error(` ✗ ${name} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// validators throw on invalid input; THREW lets us assert rejection without try/catch per case
|
|
22
|
+
const THREW = 'THREW'
|
|
23
|
+
const attempt = (validate: (v: any) => string, value: string) => {
|
|
24
|
+
try { return validate(value) } catch (err) { return THREW }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// phoneValidator and phoneValidatorOptional carry identical parsing logic and differ only in
|
|
28
|
+
// their build_validator options, so every parsing case runs against both.
|
|
29
|
+
const run_validator_tests = () => {
|
|
30
|
+
console.log("\n[phoneValidator / phoneValidatorOptional]")
|
|
31
|
+
|
|
32
|
+
const cases: [string, string, string][] = [
|
|
33
|
+
// [input, expected, description]
|
|
34
|
+
['+14155551234', '+14155551234', 'normalized E.164 passes through'],
|
|
35
|
+
['4155551234', '+14155551234', 'bare 10 digits gets a US country code'],
|
|
36
|
+
['(415) 555-1234', '+14155551234', 'formatted number is normalized'],
|
|
37
|
+
[' 415-555-1234 ', '+14155551234', 'whitespace and dashes are stripped'],
|
|
38
|
+
['311', '311', 'N11 service code passes through (pre-existing behavior)'],
|
|
39
|
+
|
|
40
|
+
// Short codes: valid SMS endpoints, but not E.164 — no country code, no '+'
|
|
41
|
+
['894546', '894546', '6-digit short code passes through unchanged'],
|
|
42
|
+
['89454', '89454', '5-digit short code passes through unchanged'],
|
|
43
|
+
['00000', '00000', 'leading zeroes are preserved, not normalized away'],
|
|
44
|
+
|
|
45
|
+
// The short-code allowance must not widen past bare 5-6 digits
|
|
46
|
+
['+1894546', THREW, 'a leading + forces the strict E.164 path'],
|
|
47
|
+
['1234', THREW, '4 digits still rejects'],
|
|
48
|
+
['1234567', THREW, '7 digits still rejects'],
|
|
49
|
+
['123456789', THREW, '9 digits still rejects'],
|
|
50
|
+
['12', THREW, 'too-few digits still rejects'],
|
|
51
|
+
['not a phone', THREW, 'junk still rejects'],
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
for (const [input, expected, description] of cases) {
|
|
55
|
+
assert(`${description} (required)`, attempt(phoneValidator.validate(), input), expected)
|
|
56
|
+
assert(`${description} (optional)`, attempt(phoneValidatorOptional.validate(), input), expected)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
assert(
|
|
60
|
+
'optional variant accepts empty string',
|
|
61
|
+
attempt(phoneValidatorOptional.validate(), ''),
|
|
62
|
+
'',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// parse_phone_number is the non-throwing wrapper used on integration payloads, where a junk
|
|
67
|
+
// number must not fail the whole request. It inherits the short-code rule from phoneValidator.
|
|
68
|
+
const run_parse_tests = () => {
|
|
69
|
+
console.log("\n[parse_phone_number]")
|
|
70
|
+
|
|
71
|
+
assert('E.164 passes through', parse_phone_number('+14155551234'), '+14155551234')
|
|
72
|
+
assert('bare 10 digits normalizes', parse_phone_number('4155551234'), '+14155551234')
|
|
73
|
+
assert('short code passes through', parse_phone_number('894546'), '894546')
|
|
74
|
+
assert('7 digits returns undefined', parse_phone_number('1234567'), undefined)
|
|
75
|
+
assert('junk returns undefined', parse_phone_number('not a phone'), undefined)
|
|
76
|
+
assert('empty string returns undefined', parse_phone_number(''), undefined)
|
|
77
|
+
assert('undefined returns undefined', parse_phone_number(undefined), undefined)
|
|
78
|
+
assert('null returns undefined', parse_phone_number(null), undefined)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Guards the display formatter against the negative-slice bug: before the length check,
|
|
82
|
+
// a 6-digit short code rendered as '+ (894) 546-'
|
|
83
|
+
const run_display_tests = () => {
|
|
84
|
+
console.log("\n[to_human_readable_phone_number]")
|
|
85
|
+
|
|
86
|
+
assert('E.164 formats with country code', to_human_readable_phone_number('+15555555555'), '+1 (555) 555-5555')
|
|
87
|
+
assert('bare 10 digits formats', to_human_readable_phone_number('5555555555'), '(555) 555-5555')
|
|
88
|
+
assert('6-digit short code is returned as-is', to_human_readable_phone_number('894546'), '894546')
|
|
89
|
+
assert('5-digit short code is returned as-is', to_human_readable_phone_number('89454'), '89454')
|
|
90
|
+
assert('311 is returned as-is', to_human_readable_phone_number('311'), '311')
|
|
91
|
+
assert('empty string returns empty string', to_human_readable_phone_number(''), '')
|
|
92
|
+
assert('undefined returns empty string', to_human_readable_phone_number(undefined), '')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const run_all = () => {
|
|
96
|
+
console.log("Running phone_validation unit tests")
|
|
97
|
+
run_validator_tests()
|
|
98
|
+
run_parse_tests()
|
|
99
|
+
run_display_tests()
|
|
100
|
+
console.log(`\nResults: ${passed} passed, ${failures} failed`)
|
|
101
|
+
if (failures > 0) process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (require.main === module) {
|
|
105
|
+
run_all()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { run_all as phone_validation_unit_tests }
|
package/test_generated.pdf
CHANGED
|
Binary file
|