@tellescope/sdk 1.256.8 → 1.256.9
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/tests/api_tests/appointment_link_expiration.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.js +12 -9
- package/lib/cjs/tests/api_tests/appointment_link_expiration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/create_note_action.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/create_note_action.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/create_note_action.test.js +237 -0
- package/lib/cjs/tests/api_tests/create_note_action.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/webhook_raw_json_number.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/webhook_raw_json_number.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/webhook_raw_json_number.test.js +262 -0
- package/lib/cjs/tests/api_tests/webhook_raw_json_number.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +236 -201
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +0 -1
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +2 -3
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/session.d.ts +0 -1
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.js +12 -9
- package/lib/esm/tests/api_tests/appointment_link_expiration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/create_note_action.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/create_note_action.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/create_note_action.test.js +233 -0
- package/lib/esm/tests/api_tests/create_note_action.test.js.map +1 -0
- package/lib/esm/tests/api_tests/webhook_raw_json_number.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/webhook_raw_json_number.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/webhook_raw_json_number.test.js +258 -0
- package/lib/esm/tests/api_tests/webhook_raw_json_number.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +237 -202
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/appointment_link_expiration.test.ts +7 -2
- package/src/tests/api_tests/create_note_action.test.ts +168 -0
- package/src/tests/api_tests/webhook_raw_json_number.test.ts +152 -0
- package/src/tests/tests.ts +52 -1
- package/test_generated.pdf +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/sdk",
|
|
3
|
-
"version": "1.256.
|
|
3
|
+
"version": "1.256.9",
|
|
4
4
|
"description": "Code for interacting with the Tellescope API",
|
|
5
5
|
"main": "./lib/cjs/sdk.js",
|
|
6
6
|
"module": "./lib/esm/sdk.js",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/tellescope-os/tellescope#readme",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tellescope/constants": "1.256.
|
|
36
|
-
"@tellescope/schema": "1.256.
|
|
37
|
-
"@tellescope/testing": "1.256.
|
|
38
|
-
"@tellescope/types-client": "1.256.
|
|
39
|
-
"@tellescope/types-models": "1.256.
|
|
40
|
-
"@tellescope/types-utilities": "1.256.
|
|
41
|
-
"@tellescope/utilities": "1.256.
|
|
42
|
-
"@tellescope/validation": "1.256.
|
|
35
|
+
"@tellescope/constants": "1.256.9",
|
|
36
|
+
"@tellescope/schema": "1.256.9",
|
|
37
|
+
"@tellescope/testing": "1.256.9",
|
|
38
|
+
"@tellescope/types-client": "1.256.9",
|
|
39
|
+
"@tellescope/types-models": "1.256.9",
|
|
40
|
+
"@tellescope/types-utilities": "1.256.9",
|
|
41
|
+
"@tellescope/utilities": "1.256.9",
|
|
42
|
+
"@tellescope/validation": "1.256.9",
|
|
43
43
|
"axios": "0.30.3",
|
|
44
44
|
"dotenv": "14.3.2",
|
|
45
45
|
"form-data": "4.0.4",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "8952d9b32afa842c1488e4eab27fe1dadc859cdb"
|
|
59
59
|
}
|
|
@@ -24,12 +24,17 @@ const decode_jwt_payload = (token: string) => {
|
|
|
24
24
|
// The API returns a /r/<token> shortlink, not the destination — follow one hop and pull the
|
|
25
25
|
// `token` query param out of the URL the patient actually lands on.
|
|
26
26
|
const resolve_redirect_token = async (link: string) => {
|
|
27
|
-
|
|
27
|
+
// The server stamps the shortlink's host from its own config (organization.customPortalURL || API_URL),
|
|
28
|
+
// which is independent of the API port under test. Follow the /r/<token> path on our test `host` so this
|
|
29
|
+
// stays port-flexible like the rest of the suite (the redirect route is served by the same API server).
|
|
30
|
+
const { pathname, search } = new URL(link)
|
|
31
|
+
const followURL = `${host}${pathname}${search}`
|
|
32
|
+
const { status, headers } = await axios.get(followURL, {
|
|
28
33
|
maxRedirects: 0,
|
|
29
34
|
validateStatus: () => true,
|
|
30
35
|
})
|
|
31
36
|
const location = headers['location']
|
|
32
|
-
if (!location) throw new Error(`Expected a redirect from ${
|
|
37
|
+
if (!location) throw new Error(`Expected a redirect from ${followURL}, got status ${status}`)
|
|
33
38
|
|
|
34
39
|
const token = new URL(location).searchParams.get('token')
|
|
35
40
|
if (!token) throw new Error(`No token query param in redirect destination: ${location}`)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import { CreateNoteAutomationAction } from "@tellescope/types-models"
|
|
5
|
+
import { log_header, async_test, assert } from "@tellescope/testing"
|
|
6
|
+
import { setup_tests } from "../setup"
|
|
7
|
+
|
|
8
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
9
|
+
|
|
10
|
+
// createNote journey action: creates a note on the enduser timeline with deterministic
|
|
11
|
+
// (merge-field) text, or AI-generated text when aiSummaryConfiguration.enabled is set.
|
|
12
|
+
// The deterministic path is fully covered here; the AI path asserts config round-trip and,
|
|
13
|
+
// when the org lacks Bedrock enablement/credits (typical dev org), the error path instead.
|
|
14
|
+
export const create_note_action_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
15
|
+
log_header("Create Note Action Tests")
|
|
16
|
+
|
|
17
|
+
const journey = await sdk.api.journeys.createOne({ title: 'Create Note Test Journey' })
|
|
18
|
+
const createdEnduserIds: string[] = []
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
/*************** Config validation round-trip ***************/
|
|
22
|
+
const validInfo = {
|
|
23
|
+
title: 'Note for {{enduser.fname}}',
|
|
24
|
+
text: 'Hi {{enduser.fname}}, this is a journey note.',
|
|
25
|
+
tags: ['auto-note'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const step = await sdk.api.automation_steps.createOne({
|
|
29
|
+
journeyId: journey.id,
|
|
30
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
31
|
+
action: { type: 'createNote', info: validInfo },
|
|
32
|
+
})
|
|
33
|
+
assert(
|
|
34
|
+
step.action.type === 'createNote' && step.action.info.title === validInfo.title && step.action.info.text === validInfo.text,
|
|
35
|
+
'createNote step round-trip failed',
|
|
36
|
+
'createNote step round-trips',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await async_test('createNote without title is rejected',
|
|
40
|
+
() => sdk.api.automation_steps.createOne({
|
|
41
|
+
journeyId: journey.id,
|
|
42
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
43
|
+
action: { type: 'createNote', info: { ...validInfo, title: undefined } as any },
|
|
44
|
+
}),
|
|
45
|
+
{ shouldError: true, onError: () => true },
|
|
46
|
+
)
|
|
47
|
+
await async_test('createNote with over-length title is rejected',
|
|
48
|
+
() => sdk.api.automation_steps.createOne({
|
|
49
|
+
journeyId: journey.id,
|
|
50
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
51
|
+
action: { type: 'createNote', info: { ...validInfo, title: 'x'.repeat(251) } },
|
|
52
|
+
}),
|
|
53
|
+
{ shouldError: true, onError: () => true },
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
/*************** Runtime: the process endpoint ***************/
|
|
57
|
+
const processFor = async (enduserId: string, info: CreateNoteAutomationAction['info']) => {
|
|
58
|
+
const automatedAction = await sdk.api.automated_actions.createOne({
|
|
59
|
+
enduserId,
|
|
60
|
+
journeyId: journey.id,
|
|
61
|
+
automationStepId: step.id,
|
|
62
|
+
event: { type: 'onJourneyStart', info: {} },
|
|
63
|
+
action: { type: 'createNote', info },
|
|
64
|
+
status: 'active',
|
|
65
|
+
processAfter: Date.now(),
|
|
66
|
+
})
|
|
67
|
+
return sdk.api.automated_actions.process({ automatedActionId: automatedAction.id })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// deterministic path: merge fields resolve, tags + source set
|
|
71
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'NoteTest' })
|
|
72
|
+
createdEnduserIds.push(enduser.id)
|
|
73
|
+
|
|
74
|
+
const result = await processFor(enduser.id, validInfo)
|
|
75
|
+
assert(result.success === true, `deterministic createNote failed: ${result.error}`, 'deterministic createNote succeeds')
|
|
76
|
+
|
|
77
|
+
const notes = await sdk.api.notes.getSome({ filter: { enduserId: enduser.id } })
|
|
78
|
+
assert(notes.length === 1, `expected 1 note, got ${notes.length}`, 'exactly one note created')
|
|
79
|
+
assert(notes[0].title === 'Note for NoteTest', `title not templated: ${notes[0].title}`, 'title merge fields resolve')
|
|
80
|
+
assert(notes[0].text === 'Hi NoteTest, this is a journey note.', `text not templated: ${notes[0].text}`, 'text merge fields resolve')
|
|
81
|
+
assert(!!notes[0].tags?.includes('auto-note'), 'tags missing on note', 'tags set on note')
|
|
82
|
+
assert(notes[0].source === 'Journey', `wrong source: ${notes[0].source}`, 'note source is Journey')
|
|
83
|
+
assert(!notes[0].hiddenFromTimeline, 'note unexpectedly hidden', 'note visible on timeline by default')
|
|
84
|
+
assert(!notes[0].aiGenerated, 'deterministic note incorrectly flagged aiGenerated', 'deterministic note not flagged as AI-generated')
|
|
85
|
+
|
|
86
|
+
// hiddenFromTimeline + no text variant
|
|
87
|
+
const hiddenResult = await processFor(enduser.id, { title: 'Hidden note', hiddenFromTimeline: true })
|
|
88
|
+
assert(hiddenResult.success === true, `hidden createNote failed: ${hiddenResult.error}`, 'hidden createNote succeeds')
|
|
89
|
+
const allNotes = await sdk.api.notes.getSome({ filter: { enduserId: enduser.id } })
|
|
90
|
+
const hiddenNote = allNotes.find(n => n.title === 'Hidden note')
|
|
91
|
+
assert(
|
|
92
|
+
!!hiddenNote && hiddenNote.hiddenFromTimeline === true && !hiddenNote.text,
|
|
93
|
+
'hidden/textless note not created correctly',
|
|
94
|
+
'hiddenFromTimeline note created without text',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
/*************** AI path ***************/
|
|
98
|
+
const aiInfo = {
|
|
99
|
+
title: 'AI note for {{enduser.fname}}',
|
|
100
|
+
aiSummaryConfiguration: {
|
|
101
|
+
enabled: true,
|
|
102
|
+
prompt: 'Write a one-sentence note stating this is a test patient.',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
const aiStep = await sdk.api.automation_steps.createOne({
|
|
106
|
+
journeyId: journey.id,
|
|
107
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
108
|
+
action: { type: 'createNote', info: aiInfo },
|
|
109
|
+
})
|
|
110
|
+
assert(
|
|
111
|
+
aiStep.action.type === 'createNote' && aiStep.action.info.aiSummaryConfiguration?.enabled === true,
|
|
112
|
+
'AI createNote config round-trip failed',
|
|
113
|
+
'AI createNote config round-trips',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const aiEnduser = await sdk.api.endusers.createOne({ fname: 'AINoteTest' })
|
|
117
|
+
createdEnduserIds.push(aiEnduser.id)
|
|
118
|
+
const aiResult = await processFor(aiEnduser.id, aiInfo)
|
|
119
|
+
if (!aiResult.success) {
|
|
120
|
+
// bedrockAIAllowed/credits are org-level and unreadable here — accept the configuration error path
|
|
121
|
+
assert(
|
|
122
|
+
/bedrock|credit|enabled|ai/i.test(aiResult.error || ''),
|
|
123
|
+
`unexpected AI createNote error: ${aiResult.error}`,
|
|
124
|
+
'AI createNote reports a configuration error (Bedrock unavailable — runtime generation skipped)',
|
|
125
|
+
)
|
|
126
|
+
} else {
|
|
127
|
+
const aiNotes = await sdk.api.notes.getSome({ filter: { enduserId: aiEnduser.id } })
|
|
128
|
+
assert(
|
|
129
|
+
aiNotes.length === 1 && !!aiNotes[0].text && aiNotes[0].title === 'AI note for AINoteTest',
|
|
130
|
+
'AI-generated note missing or malformed',
|
|
131
|
+
'AI-generated note created with non-empty text',
|
|
132
|
+
)
|
|
133
|
+
assert(aiNotes[0].aiGenerated === true, 'AI note missing aiGenerated flag', 'AI note flagged as AI-generated')
|
|
134
|
+
const conversations = await sdk.api.ai_conversations.getSome({ filter: { enduserId: aiEnduser.id } })
|
|
135
|
+
assert(
|
|
136
|
+
conversations.some(c => c.type === 'AI Note'),
|
|
137
|
+
'no AI Note ai_conversations record',
|
|
138
|
+
'AI generation logged to ai_conversations with type AI Note',
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
await sdk.api.journeys.deleteOne(journey.id).catch(() => {})
|
|
143
|
+
for (const id of createdEnduserIds) {
|
|
144
|
+
await sdk.api.endusers.deleteOne(id).catch(() => {})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Allow running this test file independently
|
|
150
|
+
if (require.main === module) {
|
|
151
|
+
const sdk = new Session({ host })
|
|
152
|
+
const sdkNonAdmin = new Session({ host })
|
|
153
|
+
|
|
154
|
+
const runTests = async () => {
|
|
155
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
156
|
+
await create_note_action_tests({ sdk, sdkNonAdmin })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
runTests()
|
|
160
|
+
.then(() => {
|
|
161
|
+
console.log("✅ Create Note Action test suite completed successfully")
|
|
162
|
+
process.exit(0)
|
|
163
|
+
})
|
|
164
|
+
.catch((error) => {
|
|
165
|
+
console.error("❌ Create Note Action test suite failed:", error)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
log_header,
|
|
7
|
+
wait,
|
|
8
|
+
} from "@tellescope/testing"
|
|
9
|
+
import { setup_tests } from "../setup"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
// Unreachable webhook target — the request will fail quickly but the API still writes
|
|
14
|
+
// the resolved payload to webhook_logs, which is what we're verifying.
|
|
15
|
+
const WEBHOOK_TARGET_URL = 'http://127.0.0.1:9'
|
|
16
|
+
|
|
17
|
+
const find_log_for_url = async (sdk: Session, url: string) => {
|
|
18
|
+
const start = Date.now()
|
|
19
|
+
while (Date.now() - start < 10_000) {
|
|
20
|
+
const logs = await sdk.api.webhook_logs.getSome({ limit: 50, sort: 'newFirst' as any }) as Array<any>
|
|
21
|
+
const match = logs.find(l => l.url === url)
|
|
22
|
+
if (match) return match
|
|
23
|
+
await wait(undefined, 500)
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Main test function that can be called independently
|
|
29
|
+
export const webhook_raw_json_number_tests = async (
|
|
30
|
+
{ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }
|
|
31
|
+
) => {
|
|
32
|
+
log_header("Webhook Raw JSON Body Numeric Placeholder Tests")
|
|
33
|
+
|
|
34
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'RawJSON', lname: 'Number' })
|
|
35
|
+
const purchase = await sdk.api.purchases.createOne({
|
|
36
|
+
enduserId: enduser.id,
|
|
37
|
+
title: 'Raw JSON Number Test Purchase',
|
|
38
|
+
cost: { amount: 129.99, currency: 'USD' },
|
|
39
|
+
processor: 'Stripe',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// create an automation step to satisfy schema validation (automationStepId is required)
|
|
43
|
+
const journey = await sdk.api.journeys.createOne({
|
|
44
|
+
title: 'Webhook Raw JSON Number Tests',
|
|
45
|
+
defaultState: 'State 1',
|
|
46
|
+
})
|
|
47
|
+
const step = await sdk.api.automation_steps.createOne({
|
|
48
|
+
journeyId: journey.id,
|
|
49
|
+
events: [{ type: 'onJourneyStart', info: {} }],
|
|
50
|
+
action: {
|
|
51
|
+
type: 'sendWebhook',
|
|
52
|
+
info: {
|
|
53
|
+
message: 'placeholder',
|
|
54
|
+
url: WEBHOOK_TARGET_URL,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await async_test(
|
|
61
|
+
'Send Webhook rawJSONBody sends unquoted {{purchase.cost.amount}} as a number and quoted as a string',
|
|
62
|
+
async () => {
|
|
63
|
+
const url = `${WEBHOOK_TARGET_URL}/raw-json-number/${purchase.id}`
|
|
64
|
+
await sdk.api.webhooks.send_automation_webhook({
|
|
65
|
+
message: 'placeholder',
|
|
66
|
+
enduserId: enduser.id,
|
|
67
|
+
automationStepId: step.id,
|
|
68
|
+
action: {
|
|
69
|
+
type: 'sendWebhook',
|
|
70
|
+
info: {
|
|
71
|
+
message: 'placeholder',
|
|
72
|
+
url,
|
|
73
|
+
rawJSONBody: '{"amount": {{purchase.cost.amount}}, "asString": "{{purchase.cost.amount}}"}',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
context: { purchaseId: purchase.id },
|
|
77
|
+
}).catch(() => {}) // the unreachable URL is expected to error after logging
|
|
78
|
+
|
|
79
|
+
const log = await find_log_for_url(sdk, url)
|
|
80
|
+
if (!log) return false
|
|
81
|
+
const payload = log.payload as any
|
|
82
|
+
return (
|
|
83
|
+
typeof payload?.amount === 'number' && payload.amount === 129.99
|
|
84
|
+
&& typeof payload?.asString === 'string' && payload.asString === '129.99'
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
{ onResult: r => r === true }
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
await async_test(
|
|
91
|
+
'Send Webhook rawJSONBody with a MISSING unquoted placeholder degrades to null instead of dropping the whole body',
|
|
92
|
+
async () => {
|
|
93
|
+
const url = `${WEBHOOK_TARGET_URL}/raw-json-missing/${purchase.id}`
|
|
94
|
+
await sdk.api.webhooks.send_automation_webhook({
|
|
95
|
+
message: 'placeholder',
|
|
96
|
+
enduserId: enduser.id,
|
|
97
|
+
automationStepId: step.id,
|
|
98
|
+
action: {
|
|
99
|
+
type: 'sendWebhook',
|
|
100
|
+
info: {
|
|
101
|
+
message: 'placeholder',
|
|
102
|
+
url,
|
|
103
|
+
// enduser has no 'NoSuchNumber' field: the unquoted placeholder would otherwise leave a
|
|
104
|
+
// literal {{...}} in a value slot, invalidating the JSON and dropping the entire payload.
|
|
105
|
+
rawJSONBody: '{"missing": {{enduser.NoSuchNumber}}, "patient": "{{enduser.fname}}"}',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
context: { purchaseId: purchase.id },
|
|
109
|
+
}).catch(() => {})
|
|
110
|
+
|
|
111
|
+
const log = await find_log_for_url(sdk, url)
|
|
112
|
+
if (!log) return false
|
|
113
|
+
const payload = log.payload as any
|
|
114
|
+
// body must still be sent (not null/empty), the missing value coerced to JSON null,
|
|
115
|
+
// and the resolvable placeholder still substituted
|
|
116
|
+
return (
|
|
117
|
+
payload && typeof payload === 'object'
|
|
118
|
+
&& payload.missing === null
|
|
119
|
+
&& payload.patient === 'RawJSON'
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
{ onResult: r => r === true }
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
try { await sdk.api.purchases.deleteOne(purchase.id) } catch {}
|
|
126
|
+
try { await sdk.api.automation_steps.deleteOne(step.id) } catch {}
|
|
127
|
+
try { await sdk.api.journeys.deleteOne(journey.id) } catch {}
|
|
128
|
+
try { await sdk.api.endusers.deleteOne(enduser.id) } catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Allow running this test file independently
|
|
133
|
+
if (require.main === module) {
|
|
134
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
135
|
+
const sdk = new Session({ host })
|
|
136
|
+
const sdkNonAdmin = new Session({ host })
|
|
137
|
+
|
|
138
|
+
const runTests = async () => {
|
|
139
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
140
|
+
await webhook_raw_json_number_tests({ sdk, sdkNonAdmin })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
runTests()
|
|
144
|
+
.then(() => {
|
|
145
|
+
console.log("✅ Webhook raw JSON number test suite completed successfully")
|
|
146
|
+
process.exit(0)
|
|
147
|
+
})
|
|
148
|
+
.catch((error) => {
|
|
149
|
+
console.error("❌ Webhook raw JSON number test suite failed:", error)
|
|
150
|
+
process.exit(1)
|
|
151
|
+
})
|
|
152
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -65,7 +65,7 @@ import { journey_delete_cancels_actions_tests } from "./api_tests/journey_delete
|
|
|
65
65
|
import { afteraction_day_of_month_delay_tests } from "./api_tests/afteraction_day_of_month_delay.test"
|
|
66
66
|
import { push_forms_to_portal_group_completion_tests } from "./api_tests/push_forms_to_portal_group_completion.test"
|
|
67
67
|
import { authenticate_enduser_via_token, setup_tests } from "./setup"
|
|
68
|
-
import { evaluate_conditional_logic_for_enduser_fields, FORM_LOGIC_CALCULATED_FIELDS, get_care_team_primary, get_flattened_fields, get_next_reminder_timestamp, object_is_empty, replace_enduser_template_values, replace_form_field_template_values, responses_satisfy_conditions, truncate_string, weighted_round_robin, YYYY_MM_DD_to_MM_DD_YYYY } from "@tellescope/utilities"
|
|
68
|
+
import { evaluate_conditional_logic_for_enduser_fields, FORM_LOGIC_CALCULATED_FIELDS, get_care_team_primary, get_flattened_fields, get_next_reminder_timestamp, object_is_empty, replace_enduser_template_values, replace_form_field_template_values, replace_purchase_template_values, responses_satisfy_conditions, safeJSONParseTemplate, truncate_string, weighted_round_robin, YYYY_MM_DD_to_MM_DD_YYYY } from "@tellescope/utilities"
|
|
69
69
|
import { DEFAULT_OPERATIONS, PLACEHOLDER_ID, ZENDESK_INTEGRATIONS_TITLE, ZOOM_TITLE } from "@tellescope/constants"
|
|
70
70
|
import {
|
|
71
71
|
schema,
|
|
@@ -144,6 +144,7 @@ import { form_started_trigger_tests } from "./api_tests/form_started_trigger.tes
|
|
|
144
144
|
import { formsort_header_auth_tests } from "./api_tests/formsort_header_auth.test";
|
|
145
145
|
import { form_submitted_trigger_tests } from "./api_tests/form_submitted_trigger.test";
|
|
146
146
|
import { start_ai_conversation_tests } from "./api_tests/start_ai_conversation.test";
|
|
147
|
+
import { create_note_action_tests } from "./api_tests/create_note_action.test";
|
|
147
148
|
import { dont_sync_to_elation_form_submission_tests } from "./api_tests/dont_sync_to_elation_form_submission.test";
|
|
148
149
|
import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
|
|
149
150
|
import { conditional_logic_medication_unit_tests } from "./unit_tests/conditional_logic_medication.test";
|
|
@@ -168,6 +169,7 @@ import { date_string_validation_tests } from "./api_tests/date_string_validation
|
|
|
168
169
|
import { enduser_session_invalidation_tests } from "./api_tests/enduser_session_invalidation.test";
|
|
169
170
|
import { enduser_cross_access_isolation_tests } from "./api_tests/enduser_cross_access_isolation.test";
|
|
170
171
|
import { calendar_event_webhook_template_tests } from "./api_tests/calendar_event_webhook_template.test";
|
|
172
|
+
import { webhook_raw_json_number_tests } from "./api_tests/webhook_raw_json_number.test";
|
|
171
173
|
import { webhook_error_handling_tests } from "./api_tests/webhook_error_handling.test";
|
|
172
174
|
import { phone_tree_enduser_condition_tests } from "./api_tests/phone_tree_enduser_condition.test";
|
|
173
175
|
|
|
@@ -13868,6 +13870,52 @@ const replace_enduser_template_values_tests = async () => {
|
|
|
13868
13870
|
await sdk.api.endusers.deleteOne(enduser.id)
|
|
13869
13871
|
}
|
|
13870
13872
|
|
|
13873
|
+
const safe_json_parse_template_tests = async () => {
|
|
13874
|
+
log_header("Safe JSON Parse Template Tests")
|
|
13875
|
+
|
|
13876
|
+
// Unquoted placeholders are valid (normalized to 0 before parsing)
|
|
13877
|
+
assert(
|
|
13878
|
+
!!safeJSONParseTemplate('{"amount": {{purchase.cost.amount}}}'),
|
|
13879
|
+
'fail unquoted placeholder', 'unquoted placeholder'
|
|
13880
|
+
)
|
|
13881
|
+
// Quoted placeholders remain valid (backward compatibility)
|
|
13882
|
+
assert(
|
|
13883
|
+
!!safeJSONParseTemplate('{"amount": "{{purchase.cost.amount}}"}'),
|
|
13884
|
+
'fail quoted placeholder', 'quoted placeholder'
|
|
13885
|
+
)
|
|
13886
|
+
// Placeholders inside a larger string remain valid
|
|
13887
|
+
assert(
|
|
13888
|
+
!!safeJSONParseTemplate('{"greeting": "Hi {{enduser.fname}}!"}'),
|
|
13889
|
+
'fail placeholder within string', 'placeholder within string'
|
|
13890
|
+
)
|
|
13891
|
+
// Mixed quoted and unquoted placeholders
|
|
13892
|
+
assert(
|
|
13893
|
+
!!safeJSONParseTemplate('{"amount": {{purchase.cost.amount}}, "asString": "{{purchase.cost.amount}}"}'),
|
|
13894
|
+
'fail mixed placeholders', 'mixed placeholders'
|
|
13895
|
+
)
|
|
13896
|
+
// Genuinely malformed JSON still rejected
|
|
13897
|
+
assert(
|
|
13898
|
+
safeJSONParseTemplate('{"amount": {{purchase.cost.amount}}') === undefined,
|
|
13899
|
+
'fail malformed template rejected', 'malformed template rejected'
|
|
13900
|
+
)
|
|
13901
|
+
assert(
|
|
13902
|
+
safeJSONParseTemplate('not json at all') === undefined,
|
|
13903
|
+
'fail non-json rejected', 'non-json rejected'
|
|
13904
|
+
)
|
|
13905
|
+
assert(safeJSONParseTemplate(undefined) === undefined, 'fail undefined template', 'undefined template')
|
|
13906
|
+
|
|
13907
|
+
// Round-trip: substitution of an unquoted placeholder yields a numeric JSON value,
|
|
13908
|
+
// while the quoted placeholder still yields a string
|
|
13909
|
+
const purchase = { title: 'Test Purchase', cost: { amount: 129.99, currency: 'USD' } } as any
|
|
13910
|
+
const substituted = replace_purchase_template_values(
|
|
13911
|
+
'{"amount": {{purchase.cost.amount}}, "asString": "{{purchase.cost.amount}}"}',
|
|
13912
|
+
purchase,
|
|
13913
|
+
)
|
|
13914
|
+
const parsed = JSON.parse(substituted)
|
|
13915
|
+
assert(typeof parsed.amount === 'number' && parsed.amount === 129.99, 'fail unquoted round-trip number', 'unquoted round-trip number')
|
|
13916
|
+
assert(typeof parsed.asString === 'string' && parsed.asString === '129.99', 'fail quoted round-trip string', 'quoted round-trip string')
|
|
13917
|
+
}
|
|
13918
|
+
|
|
13871
13919
|
const replace_form_field_template_values_tests = async () => {
|
|
13872
13920
|
log_header("Replace Form Field Template Values Tests")
|
|
13873
13921
|
|
|
@@ -15295,6 +15343,7 @@ const ip_address_form_tests = async () => {
|
|
|
15295
15343
|
await conditional_logic_medication_unit_tests()
|
|
15296
15344
|
await phone_validation_unit_tests()
|
|
15297
15345
|
await replace_enduser_template_values_tests()
|
|
15346
|
+
await safe_json_parse_template_tests()
|
|
15298
15347
|
await replace_form_field_template_values_tests()
|
|
15299
15348
|
await mfa_tests()
|
|
15300
15349
|
await setup_tests(sdk, sdkNonAdmin)
|
|
@@ -15326,6 +15375,7 @@ const ip_address_form_tests = async () => {
|
|
|
15326
15375
|
await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
|
|
15327
15376
|
await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
|
|
15328
15377
|
await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
|
|
15378
|
+
await webhook_raw_json_number_tests({ sdk, sdkNonAdmin })
|
|
15329
15379
|
await webhook_error_handling_tests({ sdk, sdkNonAdmin })
|
|
15330
15380
|
await outbound_chat_sent_trigger_tests({ sdk })
|
|
15331
15381
|
await enduser_login_rate_limits_tests({ sdk, sdkNonAdmin })
|
|
@@ -15352,6 +15402,7 @@ const ip_address_form_tests = async () => {
|
|
|
15352
15402
|
await field_redaction_tests({ sdk, sdkNonAdmin })
|
|
15353
15403
|
await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
|
|
15354
15404
|
await start_ai_conversation_tests({ sdk, sdkNonAdmin })
|
|
15405
|
+
await create_note_action_tests({ sdk, sdkNonAdmin })
|
|
15355
15406
|
await dont_sync_to_elation_form_submission_tests({ sdk, sdkNonAdmin })
|
|
15356
15407
|
await date_string_validation_tests({ sdk, sdkNonAdmin })
|
|
15357
15408
|
await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|