@tellescope/sdk 1.255.13 → 1.255.15
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 +1 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.d.ts +14 -0
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js +172 -0
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/formsort_header_auth.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/formsort_header_auth.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/formsort_header_auth.test.js +221 -0
- package/lib/cjs/tests/api_tests/formsort_header_auth.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +179 -0
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/phone_tree_enduser_condition.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/phone_tree_enduser_condition.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_tree_enduser_condition.test.js +237 -0
- package/lib/cjs/tests/api_tests/phone_tree_enduser_condition.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/send_message_model.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/send_message_model.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +148 -132
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +1 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.d.ts +14 -0
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js +168 -0
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js.map +1 -0
- package/lib/esm/tests/api_tests/formsort_header_auth.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/formsort_header_auth.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/formsort_header_auth.test.js +214 -0
- package/lib/esm/tests/api_tests/formsort_header_auth.test.js.map +1 -0
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +175 -0
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -0
- package/lib/esm/tests/api_tests/phone_tree_enduser_condition.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/phone_tree_enduser_condition.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_tree_enduser_condition.test.js +233 -0
- package/lib/esm/tests/api_tests/phone_tree_enduser_condition.test.js.map +1 -0
- package/lib/esm/tests/api_tests/send_message_model.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/send_message_model.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +148 -132
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +4 -0
- package/src/tests/api_tests/calendar_event_references_update.test.ts +108 -0
- package/src/tests/api_tests/formsort_header_auth.test.ts +179 -0
- package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +113 -0
- package/src/tests/api_tests/phone_tree_enduser_condition.test.ts +174 -0
- package/src/tests/tests.ts +8 -0
- package/test_generated.pdf +0 -0
package/src/sdk.ts
CHANGED
|
@@ -704,6 +704,9 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
|
|
|
704
704
|
upgrade_to_conference: (args: extractFields<CustomActions['phone_calls']['upgrade_to_conference']['parameters']>) => (
|
|
705
705
|
Promise<extractFields<CustomActions['phone_calls']['upgrade_to_conference']['returns']>>
|
|
706
706
|
),
|
|
707
|
+
cold_transfer: (args: extractFields<CustomActions['phone_calls']['cold_transfer']['parameters']>) => (
|
|
708
|
+
Promise<extractFields<CustomActions['phone_calls']['cold_transfer']['returns']>>
|
|
709
|
+
),
|
|
707
710
|
add_conference_attendees: (args: extractFields<CustomActions['phone_calls']['add_conference_attendees']['parameters']>) => (
|
|
708
711
|
Promise<extractFields<CustomActions['phone_calls']['add_conference_attendees']['returns']>>
|
|
709
712
|
),
|
|
@@ -1154,6 +1157,7 @@ export class Session extends SessionManager {
|
|
|
1154
1157
|
queries.phone_calls.get_report = a => this._GET(`/v1/${schema.phone_calls.customActions.get_report.path}`, a)
|
|
1155
1158
|
queries.phone_calls.authenticate_calling = a => this._POST(`/v1${schema.phone_calls.customActions.authenticate_calling.path}`, a)
|
|
1156
1159
|
queries.phone_calls.upgrade_to_conference = a => this._POST(`/v1${schema.phone_calls.customActions.upgrade_to_conference.path}`, a)
|
|
1160
|
+
queries.phone_calls.cold_transfer = a => this._POST(`/v1${schema.phone_calls.customActions.cold_transfer.path}`, a)
|
|
1157
1161
|
queries.phone_calls.add_conference_attendees = a => this._POST(`/v1${schema.phone_calls.customActions.add_conference_attendees.path}`, a)
|
|
1158
1162
|
queries.phone_calls.remove_conference_attendees = a => this._POST(`/v1${schema.phone_calls.customActions.remove_conference_attendees.path}`, a)
|
|
1159
1163
|
queries.phone_calls.end_conference = a => this._POST(`/v1${schema.phone_calls.customActions.end_conference.path}`, a)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session, EnduserSession } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
log_header,
|
|
7
|
+
} from "@tellescope/testing"
|
|
8
|
+
import { setup_tests, authenticate_enduser_via_token } from "../setup"
|
|
9
|
+
|
|
10
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
+
const businessId = '60398b1131a295e64f084ff6'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CU-86e2dvrn9: calendar_events.references switched from updatesDisabled to
|
|
15
|
+
* enduserUpdatesDisabled (schema.ts), mirroring the Enduser references behavior:
|
|
16
|
+
* - staff / API-key sessions may update references (e.g. to set or clear
|
|
17
|
+
* external appointment IDs like stale Elation references)
|
|
18
|
+
* - enduser (patient) sessions remain blocked — required because endusers DO
|
|
19
|
+
* have update access on calendar_events they attend (for cancelling)
|
|
20
|
+
*/
|
|
21
|
+
export const calendar_event_references_update_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
22
|
+
log_header("Calendar Event References Update (enduserUpdatesDisabled)")
|
|
23
|
+
|
|
24
|
+
let enduserId: string | undefined
|
|
25
|
+
let eventId: string | undefined
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Setup: enduser attending a calendar event, so the enduser session has update access to it
|
|
29
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
30
|
+
email: `references-update-${Date.now()}@example.com`,
|
|
31
|
+
fname: 'References',
|
|
32
|
+
lname: 'UpdateTest',
|
|
33
|
+
})
|
|
34
|
+
enduserId = enduser.id
|
|
35
|
+
|
|
36
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
37
|
+
title: "References Update Test Event",
|
|
38
|
+
durationInMinutes: 30,
|
|
39
|
+
startTimeInMS: Date.now(),
|
|
40
|
+
attendees: [{ id: enduser.id, type: 'enduser' }],
|
|
41
|
+
})
|
|
42
|
+
eventId = event.id
|
|
43
|
+
|
|
44
|
+
const enduserSDK = new EnduserSession({ host, businessId })
|
|
45
|
+
await authenticate_enduser_via_token(sdk, enduserSDK, { id: enduser.id })
|
|
46
|
+
|
|
47
|
+
// ---- Staff/API session can update references ----
|
|
48
|
+
const staffReferences = [{ type: 'Elation', id: 'elation-appointment-123' }]
|
|
49
|
+
await async_test(
|
|
50
|
+
'staff can update calendar_events.references',
|
|
51
|
+
() => sdk.api.calendar_events.updateOne(event.id, { references: staffReferences }),
|
|
52
|
+
{ onResult: e => e.references?.length === 1 && e.references[0].type === 'Elation' && e.references[0].id === 'elation-appointment-123' },
|
|
53
|
+
)
|
|
54
|
+
await async_test(
|
|
55
|
+
'staff references update persisted',
|
|
56
|
+
() => sdk.api.calendar_events.getOne(event.id),
|
|
57
|
+
{ onResult: e => e.references?.length === 1 && e.references[0].type === 'Elation' && e.references[0].id === 'elation-appointment-123' },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// ---- Enduser session (attendee with update access) is still blocked on references ----
|
|
61
|
+
await async_test(
|
|
62
|
+
'enduser cannot update calendar_events.references',
|
|
63
|
+
() => enduserSDK.api.calendar_events.updateOne(event.id, { references: [{ type: 'Attacker', id: 'attacker-reference' }] } as any),
|
|
64
|
+
{ shouldError: true, onError: e => e.message === "references cannot be updated by endusers" },
|
|
65
|
+
)
|
|
66
|
+
await async_test(
|
|
67
|
+
'rejected enduser references update did not persist',
|
|
68
|
+
() => sdk.api.calendar_events.getOne(event.id),
|
|
69
|
+
{ onResult: e => e.references?.length === 1 && e.references[0].type === 'Elation' && e.references[0].id === 'elation-appointment-123' },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// ---- Positive control: the guard is field-scoped — enduser can still cancel the event ----
|
|
73
|
+
await async_test(
|
|
74
|
+
'enduser can still perform allowed update (cancelledAt)',
|
|
75
|
+
() => enduserSDK.api.calendar_events.updateOne(event.id, { cancelledAt: new Date() }),
|
|
76
|
+
{ onResult: e => !!e.cancelledAt },
|
|
77
|
+
)
|
|
78
|
+
} finally {
|
|
79
|
+
if (eventId) {
|
|
80
|
+
try { await sdk.api.calendar_events.deleteOne(eventId) } catch {}
|
|
81
|
+
}
|
|
82
|
+
if (enduserId) {
|
|
83
|
+
try { await sdk.api.endusers.deleteOne(enduserId) } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Allow running this test file independently
|
|
89
|
+
if (require.main === module) {
|
|
90
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
91
|
+
const sdk = new Session({ host })
|
|
92
|
+
const sdkNonAdmin = new Session({ host })
|
|
93
|
+
|
|
94
|
+
const runTests = async () => {
|
|
95
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
96
|
+
await calendar_event_references_update_tests({ sdk, sdkNonAdmin })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
runTests()
|
|
100
|
+
.then(() => {
|
|
101
|
+
console.log("✅ Calendar event references update test suite completed successfully")
|
|
102
|
+
process.exit(0)
|
|
103
|
+
})
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
console.error("❌ Calendar event references update test suite failed:", error)
|
|
106
|
+
process.exit(1)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
import { Session } from "../../sdk"
|
|
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
|
+
// same seeded test API key used by formsort_tests (tests.ts)
|
|
11
|
+
const TEST_API_KEY = "9d4f9dff00f60df2690a16da2cb848f289b447614ad9bef850e54af09a1fbf7a"
|
|
12
|
+
// seeded second-tenant key (sdkOther in tests.ts) — valid, but for a different organization
|
|
13
|
+
const OTHER_TENANT_API_KEY = "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7"
|
|
14
|
+
const TEST_EMAIL = "formsort-header-auth@tellescope.com"
|
|
15
|
+
|
|
16
|
+
export const formsort_header_auth_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
17
|
+
log_header("FormSort Header Auth Tests")
|
|
18
|
+
|
|
19
|
+
const form = await sdk.api.forms.createOne({ title: "FormSort Header Auth" })
|
|
20
|
+
|
|
21
|
+
const buildUrl = (route: 'formsort' | 'form-ingestion', pathSegment: string) => {
|
|
22
|
+
const url = new URL(`${host}/v1/webhooks/${route}/${pathSegment}`)
|
|
23
|
+
url.searchParams.set('formId', form.id)
|
|
24
|
+
url.searchParams.set('returnJSON', 'true')
|
|
25
|
+
return url.toString()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bodyFor = (responder_uuid: string) => ({
|
|
29
|
+
answers: [{ key: 'email', value: TEST_EMAIL }],
|
|
30
|
+
responder_uuid,
|
|
31
|
+
finalized: true,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const formResponseIds: string[] = []
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 1. Valid key via Authorization: API_KEY header (placeholder path segment)
|
|
38
|
+
const headerAuthResult = await axios.post(
|
|
39
|
+
buildUrl('formsort', 'key-in-header'),
|
|
40
|
+
bodyFor("fha-1"),
|
|
41
|
+
{ headers: { Authorization: `API_KEY ${TEST_API_KEY}` } },
|
|
42
|
+
)
|
|
43
|
+
assert(
|
|
44
|
+
headerAuthResult.status === 200 && !!headerAuthResult.data?.formResponseId && !!headerAuthResult.data?.enduserId,
|
|
45
|
+
`Header auth failed: ${headerAuthResult.status} ${JSON.stringify(headerAuthResult.data)}`,
|
|
46
|
+
"Valid API_KEY header authenticates",
|
|
47
|
+
)
|
|
48
|
+
if (headerAuthResult.data?.formResponseId) formResponseIds.push(headerAuthResult.data.formResponseId)
|
|
49
|
+
|
|
50
|
+
await async_test(
|
|
51
|
+
"Form response created via header auth is retrievable",
|
|
52
|
+
() => sdk.api.form_responses.getOne(headerAuthResult.data.formResponseId),
|
|
53
|
+
{ onResult: fr => fr.formId === form.id && fr.source === 'Formsort' && !!fr.submittedAt },
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// 2. Invalid key in header rejected, even with a valid key in the path (header takes precedence)
|
|
57
|
+
await async_test(
|
|
58
|
+
"Invalid API_KEY header rejected despite valid key in path",
|
|
59
|
+
() => axios.post(
|
|
60
|
+
buildUrl('formsort', TEST_API_KEY),
|
|
61
|
+
bodyFor("fha-2"),
|
|
62
|
+
{ headers: { Authorization: `API_KEY not-a-real-key` } },
|
|
63
|
+
),
|
|
64
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// 3. Back-compat: valid key in path, no Authorization header
|
|
68
|
+
const pathAuthResult = await axios.post(buildUrl('formsort', TEST_API_KEY), bodyFor("fha-3"))
|
|
69
|
+
assert(
|
|
70
|
+
pathAuthResult.status === 200 && !!pathAuthResult.data?.formResponseId,
|
|
71
|
+
`Path auth failed: ${pathAuthResult.status} ${JSON.stringify(pathAuthResult.data)}`,
|
|
72
|
+
"Path-segment API key still authenticates (no header)",
|
|
73
|
+
)
|
|
74
|
+
if (pathAuthResult.data?.formResponseId) formResponseIds.push(pathAuthResult.data.formResponseId)
|
|
75
|
+
|
|
76
|
+
// 4. Back-compat: non-API_KEY Authorization scheme falls through to path key
|
|
77
|
+
const bearerFallthroughResult = await axios.post(
|
|
78
|
+
buildUrl('formsort', TEST_API_KEY),
|
|
79
|
+
bodyFor("fha-4"),
|
|
80
|
+
{ headers: { Authorization: 'Bearer unrelated-token' } },
|
|
81
|
+
)
|
|
82
|
+
assert(
|
|
83
|
+
bearerFallthroughResult.status === 200 && !!bearerFallthroughResult.data?.formResponseId,
|
|
84
|
+
`Bearer fallthrough failed: ${bearerFallthroughResult.status} ${JSON.stringify(bearerFallthroughResult.data)}`,
|
|
85
|
+
"Non-API_KEY Authorization scheme falls through to path key",
|
|
86
|
+
)
|
|
87
|
+
if (bearerFallthroughResult.data?.formResponseId) formResponseIds.push(bearerFallthroughResult.data.formResponseId)
|
|
88
|
+
|
|
89
|
+
// 5. No valid key in header or path
|
|
90
|
+
await async_test(
|
|
91
|
+
"No valid key anywhere rejected",
|
|
92
|
+
() => axios.post(buildUrl('formsort', 'not-a-real-key'), bodyFor("fha-5")),
|
|
93
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// 6. Alias route accepts header auth too
|
|
97
|
+
const aliasResult = await axios.post(
|
|
98
|
+
buildUrl('form-ingestion', 'key-in-header'),
|
|
99
|
+
bodyFor("fha-6"),
|
|
100
|
+
{ headers: { Authorization: `API_KEY ${TEST_API_KEY}` } },
|
|
101
|
+
)
|
|
102
|
+
assert(
|
|
103
|
+
aliasResult.status === 200 && !!aliasResult.data?.formResponseId,
|
|
104
|
+
`form-ingestion header auth failed: ${aliasResult.status} ${JSON.stringify(aliasResult.data)}`,
|
|
105
|
+
"form-ingestion alias accepts API_KEY header",
|
|
106
|
+
)
|
|
107
|
+
if (aliasResult.data?.formResponseId) formResponseIds.push(aliasResult.data.formResponseId)
|
|
108
|
+
|
|
109
|
+
// 7. Back-compat: API_KEY scheme with no value falls through to the path key
|
|
110
|
+
const emptyValueResult = await axios.post(
|
|
111
|
+
buildUrl('formsort', TEST_API_KEY),
|
|
112
|
+
bodyFor("fha-7"),
|
|
113
|
+
{ headers: { Authorization: 'API_KEY' } },
|
|
114
|
+
)
|
|
115
|
+
assert(
|
|
116
|
+
emptyValueResult.status === 200 && !!emptyValueResult.data?.formResponseId,
|
|
117
|
+
`Empty-value header fallthrough failed: ${emptyValueResult.status} ${JSON.stringify(emptyValueResult.data)}`,
|
|
118
|
+
"API_KEY scheme without value falls through to path key",
|
|
119
|
+
)
|
|
120
|
+
if (emptyValueResult.data?.formResponseId) formResponseIds.push(emptyValueResult.data.formResponseId)
|
|
121
|
+
|
|
122
|
+
// 8. Back-compat: single-token (schemeless) Authorization header falls through to the path key
|
|
123
|
+
const singleTokenResult = await axios.post(
|
|
124
|
+
buildUrl('formsort', TEST_API_KEY),
|
|
125
|
+
bodyFor("fha-8"),
|
|
126
|
+
{ headers: { Authorization: 'some-opaque-token' } },
|
|
127
|
+
)
|
|
128
|
+
assert(
|
|
129
|
+
singleTokenResult.status === 200 && !!singleTokenResult.data?.formResponseId,
|
|
130
|
+
`Single-token header fallthrough failed: ${singleTokenResult.status} ${JSON.stringify(singleTokenResult.data)}`,
|
|
131
|
+
"Schemeless Authorization header falls through to path key",
|
|
132
|
+
)
|
|
133
|
+
if (singleTokenResult.data?.formResponseId) formResponseIds.push(singleTokenResult.data.formResponseId)
|
|
134
|
+
|
|
135
|
+
// 9. When header and path both hold VALID keys, the header key's organization scopes the request:
|
|
136
|
+
// the formId belongs to the primary tenant, so an other-tenant header key must fail form lookup (400),
|
|
137
|
+
// proving the path key's org is not used
|
|
138
|
+
await async_test(
|
|
139
|
+
"Valid other-tenant header key overrides valid path key (org scoping)",
|
|
140
|
+
() => axios.post(
|
|
141
|
+
buildUrl('formsort', TEST_API_KEY),
|
|
142
|
+
bodyFor("fha-9"),
|
|
143
|
+
{ headers: { Authorization: `API_KEY ${OTHER_TENANT_API_KEY}` } },
|
|
144
|
+
),
|
|
145
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 400 && e.response?.data === 'formId Invalid' },
|
|
146
|
+
)
|
|
147
|
+
} finally {
|
|
148
|
+
for (const id of formResponseIds) {
|
|
149
|
+
try { await sdk.api.form_responses.deleteOne(id) } catch (err) { /* may not exist */ }
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const enduser = await sdk.api.endusers.getOne({ email: TEST_EMAIL })
|
|
153
|
+
await sdk.api.endusers.deleteOne(enduser.id)
|
|
154
|
+
} catch (err) { /* may not exist */ }
|
|
155
|
+
try { await sdk.api.forms.deleteOne(form.id) } catch (err) { /* may not exist */ }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Allow running this test file independently
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
162
|
+
const sdk = new Session({ host })
|
|
163
|
+
const sdkNonAdmin = new Session({ host })
|
|
164
|
+
|
|
165
|
+
const runTests = async () => {
|
|
166
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
167
|
+
await formsort_header_auth_tests({ sdk, sdkNonAdmin })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
runTests()
|
|
171
|
+
.then(() => {
|
|
172
|
+
console.log("✅ FormSort header auth tests completed successfully")
|
|
173
|
+
process.exit(0)
|
|
174
|
+
})
|
|
175
|
+
.catch((error) => {
|
|
176
|
+
console.error("❌ FormSort header auth tests failed:", error)
|
|
177
|
+
process.exit(1)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
handleAnyError,
|
|
7
|
+
log_header,
|
|
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
|
+
const NONEXISTENT_MONGO_ID = '60398b1131a295e64f084fff'
|
|
14
|
+
|
|
15
|
+
// Validation-path tests for the cold transfer endpoint. All assertions below hit
|
|
16
|
+
// validations that run before any Twilio access, so no Twilio config is needed.
|
|
17
|
+
export const phone_calls_cold_transfer_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
18
|
+
log_header("Phone Calls Cold Transfer Tests")
|
|
19
|
+
|
|
20
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'Cold', lname: 'Transfer' })
|
|
21
|
+
const targetUsers: { id: string }[] = []
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const inboundCall = await sdk.api.phone_calls.createOne({
|
|
25
|
+
enduserId: enduser.id,
|
|
26
|
+
inbound: true,
|
|
27
|
+
externalId: 'CAcoldtransfertestinbound000000001',
|
|
28
|
+
from: '+15550001111',
|
|
29
|
+
to: '+15550002222',
|
|
30
|
+
userId: sdk.userInfo.id,
|
|
31
|
+
})
|
|
32
|
+
const outboundCall = await sdk.api.phone_calls.createOne({
|
|
33
|
+
enduserId: enduser.id,
|
|
34
|
+
inbound: false,
|
|
35
|
+
externalId: 'CAcoldtransfertestoutbound00000001',
|
|
36
|
+
from: '+15550002222',
|
|
37
|
+
to: '+15550001111',
|
|
38
|
+
userId: sdk.userInfo.id,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// throwaway target users (never modify existing users' roles)
|
|
42
|
+
const disabledTargetUser = await sdk.api.users.createOne({
|
|
43
|
+
email: 'cold.transfer.disabled@tellescope.com',
|
|
44
|
+
verifiedEmail: true,
|
|
45
|
+
notificationEmailsDisabled: true,
|
|
46
|
+
disableIncomingPhoneCalls: true,
|
|
47
|
+
})
|
|
48
|
+
targetUsers.push(disabledTargetUser)
|
|
49
|
+
|
|
50
|
+
await async_test(
|
|
51
|
+
"cold_transfer: cannot transfer to yourself",
|
|
52
|
+
() => sdk.api.phone_calls.cold_transfer({ callSid: inboundCall.externalId, targetUserId: sdk.userInfo.id }),
|
|
53
|
+
{ shouldError: true, onError: (e: any) => e.message === "Cannot transfer a call to yourself" }
|
|
54
|
+
)
|
|
55
|
+
await async_test(
|
|
56
|
+
"cold_transfer: bogus callSid rejected",
|
|
57
|
+
() => sdk.api.phone_calls.cold_transfer({ callSid: 'CAdoesnotexist0000000000000000000', targetUserId: disabledTargetUser.id }),
|
|
58
|
+
{ shouldError: true, onError: (e: any) => e.message === "Call not found" }
|
|
59
|
+
)
|
|
60
|
+
await async_test(
|
|
61
|
+
"cold_transfer: outbound calls rejected",
|
|
62
|
+
() => sdk.api.phone_calls.cold_transfer({ callSid: outboundCall.externalId, targetUserId: disabledTargetUser.id }),
|
|
63
|
+
{ shouldError: true, onError: (e: any) => e.message === "Only inbound calls can be transferred" }
|
|
64
|
+
)
|
|
65
|
+
// conferenceId is not settable via the public API (only set internally by conference webhooks),
|
|
66
|
+
// so the "Conference calls cannot be transferred" branch is covered by manual E2E testing only
|
|
67
|
+
await async_test(
|
|
68
|
+
"cold_transfer: nonexistent target user rejected",
|
|
69
|
+
() => sdk.api.phone_calls.cold_transfer({ callSid: inboundCall.externalId, targetUserId: NONEXISTENT_MONGO_ID }),
|
|
70
|
+
{ shouldError: true, onError: (e: any) => e.message === "Target user not found" }
|
|
71
|
+
)
|
|
72
|
+
await async_test(
|
|
73
|
+
"cold_transfer: target user with incoming calls disabled rejected",
|
|
74
|
+
() => sdk.api.phone_calls.cold_transfer({ callSid: inboundCall.externalId, targetUserId: disabledTargetUser.id }),
|
|
75
|
+
{ shouldError: true, onError: (e: any) => e.message === "Target user has incoming calls disabled" }
|
|
76
|
+
)
|
|
77
|
+
// non-admin either can't see the call (Call not found) or is stopped by a later
|
|
78
|
+
// validation (target has incoming calls disabled) — errors on every path
|
|
79
|
+
await async_test(
|
|
80
|
+
"cold_transfer: non-admin cannot transfer an inaccessible call",
|
|
81
|
+
() => sdkNonAdmin.api.phone_calls.cold_transfer({ callSid: inboundCall.externalId, targetUserId: disabledTargetUser.id }),
|
|
82
|
+
handleAnyError
|
|
83
|
+
)
|
|
84
|
+
} finally {
|
|
85
|
+
// deleting the enduser cascades deletion of its phone_calls records
|
|
86
|
+
await Promise.all([
|
|
87
|
+
sdk.api.endusers.deleteOne(enduser.id),
|
|
88
|
+
...targetUsers.map(u => sdk.api.users.deleteOne(u.id)),
|
|
89
|
+
]).catch(console.error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Allow running this test file independently
|
|
94
|
+
if (require.main === module) {
|
|
95
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
96
|
+
const sdk = new Session({ host })
|
|
97
|
+
const sdkNonAdmin = new Session({ host })
|
|
98
|
+
|
|
99
|
+
const runTests = async () => {
|
|
100
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
101
|
+
await phone_calls_cold_transfer_tests({ sdk, sdkNonAdmin })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
runTests()
|
|
105
|
+
.then(() => {
|
|
106
|
+
console.log("✅ Phone calls cold transfer test suite completed successfully")
|
|
107
|
+
process.exit(0)
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
console.error("❌ Phone calls cold transfer test suite failed:", error)
|
|
111
|
+
process.exit(1)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
assert,
|
|
6
|
+
async_test,
|
|
7
|
+
handleAnyError,
|
|
8
|
+
log_header,
|
|
9
|
+
} from "@tellescope/testing"
|
|
10
|
+
import { PhoneTreeNode } from "@tellescope/types-models"
|
|
11
|
+
import { setup_tests } from "../setup"
|
|
12
|
+
|
|
13
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
14
|
+
|
|
15
|
+
const SPLIT_NODE_ID = 'enduser-condition-split-node'
|
|
16
|
+
const VIP_NODE_ID = 'vip-play-message-node'
|
|
17
|
+
const ELSE_NODE_ID = 'else-play-message-node'
|
|
18
|
+
|
|
19
|
+
const valid_condition_split_nodes: PhoneTreeNode[] = [
|
|
20
|
+
{
|
|
21
|
+
id: SPLIT_NODE_ID,
|
|
22
|
+
action: {
|
|
23
|
+
type: 'Enduser Condition Split',
|
|
24
|
+
info: {
|
|
25
|
+
branches: [
|
|
26
|
+
{ name: 'VIP', enduserCondition: { $and: [{ condition: { tags: 'VIP' } }] } },
|
|
27
|
+
{ name: 'Everyone Else' }, // no condition — catch-all
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
events: [{ type: 'Start', parentId: SPLIT_NODE_ID, info: {} }],
|
|
32
|
+
flowchartUI: { x: 0, y: 0 },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: VIP_NODE_ID,
|
|
36
|
+
action: {
|
|
37
|
+
type: 'Play Message',
|
|
38
|
+
info: { playback: { type: 'Say', info: { script: 'Welcome, VIP caller' } } },
|
|
39
|
+
},
|
|
40
|
+
events: [{ type: 'On Condition Branch', parentId: SPLIT_NODE_ID, info: { branch: 'VIP' } }],
|
|
41
|
+
flowchartUI: { x: -100, y: 100 },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: ELSE_NODE_ID,
|
|
45
|
+
action: {
|
|
46
|
+
type: 'Play Message',
|
|
47
|
+
info: { playback: { type: 'Say', info: { script: 'Welcome' } } },
|
|
48
|
+
},
|
|
49
|
+
events: [{ type: 'On Condition Branch', parentId: SPLIT_NODE_ID, info: { branch: 'Everyone Else' } }],
|
|
50
|
+
flowchartUI: { x: 100, y: 100 },
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
export const phone_tree_enduser_condition_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
55
|
+
log_header("Phone Tree Enduser Condition Split Tests")
|
|
56
|
+
|
|
57
|
+
const treeIds: string[] = []
|
|
58
|
+
try {
|
|
59
|
+
// valid Enduser Condition Split node with On Condition Branch children saves
|
|
60
|
+
const tree = await sdk.api.phone_trees.createOne({
|
|
61
|
+
number: '+15005550006',
|
|
62
|
+
isActive: false,
|
|
63
|
+
nodes: valid_condition_split_nodes,
|
|
64
|
+
})
|
|
65
|
+
treeIds.push(tree.id)
|
|
66
|
+
assert(!!tree.id, 'failed to create tree with Enduser Condition Split', 'create tree with Enduser Condition Split')
|
|
67
|
+
|
|
68
|
+
const loaded = await sdk.api.phone_trees.getOne(tree.id)
|
|
69
|
+
const loadedSplit = loaded.nodes.find(n => n.action.type === 'Enduser Condition Split')
|
|
70
|
+
assert(
|
|
71
|
+
loadedSplit?.action.type === 'Enduser Condition Split'
|
|
72
|
+
&& loadedSplit.action.info.branches.length === 2
|
|
73
|
+
&& loadedSplit.action.info.branches[0].name === 'VIP'
|
|
74
|
+
&& !!loadedSplit.action.info.branches[0].enduserCondition
|
|
75
|
+
&& loadedSplit.action.info.branches[1].enduserCondition === undefined,
|
|
76
|
+
'branches did not round-trip',
|
|
77
|
+
'branches (conditions + catch-all) round-trip on read',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// regression: existing Conditional Split shape is unaffected
|
|
81
|
+
const conditionalSplitTree = await sdk.api.phone_trees.createOne({
|
|
82
|
+
number: '+15005550006',
|
|
83
|
+
isActive: false,
|
|
84
|
+
nodes: [{
|
|
85
|
+
id: 'conditional-split-node',
|
|
86
|
+
// timezone + weeklyAvailabilities included to match the webapp's default Conditional Split shape
|
|
87
|
+
action: { type: 'Conditional Split', info: { timezone: 'US/Eastern', weeklyAvailabilities: [], hasCareTeam: true } },
|
|
88
|
+
events: [{ type: 'Start', parentId: 'conditional-split-node', info: {} }],
|
|
89
|
+
flowchartUI: { x: 0, y: 0 },
|
|
90
|
+
}],
|
|
91
|
+
})
|
|
92
|
+
treeIds.push(conditionalSplitTree.id)
|
|
93
|
+
assert(!!conditionalSplitTree.id, 'failed to create Conditional Split tree', 'existing Conditional Split still saves (regression)')
|
|
94
|
+
|
|
95
|
+
// invalid: enduserCondition must be an object
|
|
96
|
+
await async_test(
|
|
97
|
+
"non-object enduserCondition is rejected",
|
|
98
|
+
() => sdk.api.phone_trees.createOne({
|
|
99
|
+
number: '+15005550006',
|
|
100
|
+
isActive: false,
|
|
101
|
+
nodes: [{
|
|
102
|
+
id: SPLIT_NODE_ID,
|
|
103
|
+
action: {
|
|
104
|
+
type: 'Enduser Condition Split',
|
|
105
|
+
info: { branches: [{ name: 'Bad', enduserCondition: "not-an-object" as any }] },
|
|
106
|
+
},
|
|
107
|
+
events: [{ type: 'Start', parentId: SPLIT_NODE_ID, info: {} }],
|
|
108
|
+
flowchartUI: { x: 0, y: 0 },
|
|
109
|
+
}],
|
|
110
|
+
}).then(t => { treeIds.push(t.id); return t }), // cleanup if it unexpectedly passes
|
|
111
|
+
handleAnyError,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// invalid: branches must include a name
|
|
115
|
+
await async_test(
|
|
116
|
+
"branch without a name is rejected",
|
|
117
|
+
() => sdk.api.phone_trees.createOne({
|
|
118
|
+
number: '+15005550006',
|
|
119
|
+
isActive: false,
|
|
120
|
+
nodes: [{
|
|
121
|
+
id: SPLIT_NODE_ID,
|
|
122
|
+
action: {
|
|
123
|
+
type: 'Enduser Condition Split',
|
|
124
|
+
info: { branches: [{ enduserCondition: {} } as any] },
|
|
125
|
+
},
|
|
126
|
+
events: [{ type: 'Start', parentId: SPLIT_NODE_ID, info: {} }],
|
|
127
|
+
flowchartUI: { x: 0, y: 0 },
|
|
128
|
+
}],
|
|
129
|
+
}).then(t => { treeIds.push(t.id); return t }),
|
|
130
|
+
handleAnyError,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// invalid: On Condition Branch events must include a branch value
|
|
134
|
+
await async_test(
|
|
135
|
+
"On Condition Branch event without branch is rejected",
|
|
136
|
+
() => sdk.api.phone_trees.createOne({
|
|
137
|
+
number: '+15005550006',
|
|
138
|
+
isActive: false,
|
|
139
|
+
nodes: valid_condition_split_nodes.map(n => (
|
|
140
|
+
n.id !== VIP_NODE_ID
|
|
141
|
+
? n
|
|
142
|
+
: { ...n, events: [{ type: 'On Condition Branch', parentId: SPLIT_NODE_ID, info: {} as any }] }
|
|
143
|
+
)),
|
|
144
|
+
}).then(t => { treeIds.push(t.id); return t }),
|
|
145
|
+
handleAnyError,
|
|
146
|
+
)
|
|
147
|
+
} finally {
|
|
148
|
+
for (const id of treeIds) {
|
|
149
|
+
await sdk.api.phone_trees.deleteOne(id).catch(console.error)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Allow running this test file independently
|
|
155
|
+
if (require.main === module) {
|
|
156
|
+
console.log(`Using API URL: ${host}`)
|
|
157
|
+
const sdk = new Session({ host })
|
|
158
|
+
const sdkNonAdmin = new Session({ host })
|
|
159
|
+
|
|
160
|
+
const runTests = async () => {
|
|
161
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
162
|
+
await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
runTests()
|
|
166
|
+
.then(() => {
|
|
167
|
+
console.log("✅ Phone tree enduser condition test suite completed successfully")
|
|
168
|
+
process.exit(0)
|
|
169
|
+
})
|
|
170
|
+
.catch((error) => {
|
|
171
|
+
console.error("❌ Phone tree enduser condition test suite failed:", error)
|
|
172
|
+
process.exit(1)
|
|
173
|
+
})
|
|
174
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
|
|
82
82
|
import fs from "fs"
|
|
83
83
|
import { load_inbox_data_tests } from "./api_tests/load_inbox_data.test";
|
|
84
|
+
import { phone_calls_cold_transfer_tests } from "./api_tests/phone_calls_cold_transfer.test";
|
|
84
85
|
import { enduser_login_tests } from "./api_tests/enduser_login.test";
|
|
85
86
|
import { enduser_login_rate_limits_tests } from "./api_tests/enduser_login_rate_limits.test";
|
|
86
87
|
import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test";
|
|
@@ -120,6 +121,7 @@ import { database_cascade_delete_tests } from "./api_tests/database_cascade_dele
|
|
|
120
121
|
import { ai_conversations_tests } from "./api_tests/ai_conversations.test";
|
|
121
122
|
import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
|
|
122
123
|
import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
|
|
124
|
+
import { formsort_header_auth_tests } from "./api_tests/formsort_header_auth.test";
|
|
123
125
|
import { form_submitted_trigger_tests } from "./api_tests/form_submitted_trigger.test";
|
|
124
126
|
import { dont_sync_to_elation_form_submission_tests } from "./api_tests/dont_sync_to_elation_form_submission.test";
|
|
125
127
|
import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
|
|
@@ -127,6 +129,7 @@ import { conditional_logic_medication_unit_tests } from "./unit_tests/conditiona
|
|
|
127
129
|
import { elation_user_id_tests } from "./api_tests/elation_user_id.test";
|
|
128
130
|
import { organization_settings_duplicates_tests } from "./api_tests/organization_settings_duplicates.test";
|
|
129
131
|
import { calendar_events_bulk_update_tests } from "./api_tests/calendar_events_bulk_update.test";
|
|
132
|
+
import { calendar_event_references_update_tests } from "./api_tests/calendar_event_references_update.test";
|
|
130
133
|
import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
|
|
131
134
|
import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
|
|
132
135
|
import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
|
|
@@ -140,6 +143,7 @@ import { enduser_session_invalidation_tests } from "./api_tests/enduser_session_
|
|
|
140
143
|
import { enduser_cross_access_isolation_tests } from "./api_tests/enduser_cross_access_isolation.test";
|
|
141
144
|
import { calendar_event_webhook_template_tests } from "./api_tests/calendar_event_webhook_template.test";
|
|
142
145
|
import { webhook_error_handling_tests } from "./api_tests/webhook_error_handling.test";
|
|
146
|
+
import { phone_tree_enduser_condition_tests } from "./api_tests/phone_tree_enduser_condition.test";
|
|
143
147
|
|
|
144
148
|
const UniquenessViolationMessage = 'Uniqueness Violation'
|
|
145
149
|
|
|
@@ -15087,6 +15091,7 @@ const ip_address_form_tests = async () => {
|
|
|
15087
15091
|
await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
|
|
15088
15092
|
await dont_sync_to_elation_form_submission_tests({ sdk, sdkNonAdmin })
|
|
15089
15093
|
await date_string_validation_tests({ sdk, sdkNonAdmin })
|
|
15094
|
+
await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
|
|
15090
15095
|
await openloop_webhooks_tests({ sdk, sdkNonAdmin })
|
|
15091
15096
|
await mdi_webhooks_tests({ sdk, sdkNonAdmin })
|
|
15092
15097
|
await integrations_redacted_tests({ sdk, sdkNonAdmin })
|
|
@@ -15122,6 +15127,7 @@ const ip_address_form_tests = async () => {
|
|
|
15122
15127
|
await afteraction_day_of_month_delay_tests({ sdk, sdkNonAdmin })
|
|
15123
15128
|
await bulk_assignment_tests({ sdk, sdkNonAdmin })
|
|
15124
15129
|
await formsort_tests()
|
|
15130
|
+
await formsort_header_auth_tests({ sdk, sdkNonAdmin })
|
|
15125
15131
|
await self_serve_appointment_booking_tests()
|
|
15126
15132
|
await test_ticket_automation_assignment_and_optimization()
|
|
15127
15133
|
await monthly_availability_restrictions_tests({ sdk, sdkNonAdmin })
|
|
@@ -15131,6 +15137,7 @@ const ip_address_form_tests = async () => {
|
|
|
15131
15137
|
await inbox_threads_building_tests()
|
|
15132
15138
|
await inbox_threads_loading_tests()
|
|
15133
15139
|
await load_inbox_data_tests({ sdk, sdkNonAdmin })
|
|
15140
|
+
await phone_calls_cold_transfer_tests({ sdk, sdkNonAdmin })
|
|
15134
15141
|
await enduser_observations_acknowledge_tests({ sdk, sdkNonAdmin })
|
|
15135
15142
|
await nutrition_tracking_tests({ sdk, sdkNonAdmin })
|
|
15136
15143
|
await translations_tests({ sdk, sdkNonAdmin })
|
|
@@ -15142,6 +15149,7 @@ const ip_address_form_tests = async () => {
|
|
|
15142
15149
|
await ip_address_form_tests()
|
|
15143
15150
|
await bulk_update_tests()
|
|
15144
15151
|
await calendar_events_bulk_update_tests({ sdk })
|
|
15152
|
+
await calendar_event_references_update_tests({ sdk, sdkNonAdmin })
|
|
15145
15153
|
await cancel_upcoming_appointments_journey_action_test()
|
|
15146
15154
|
await multi_tenant_tests() // should come right after setup tests
|
|
15147
15155
|
await sync_tests_with_access_tags() // should come directly after setup to avoid extra sync values
|
package/test_generated.pdf
CHANGED
|
Binary file
|