@tellescope/sdk 1.246.2 → 1.248.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/sdk.d.ts +7 -1
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +2 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.js +142 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +243 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts +13 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.js +818 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js +429 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js +273 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js +370 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +108 -24
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +303 -180
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +7 -1
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +2 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/date_string_validation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.js +138 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +239 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts +13 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.js +814 -0
- package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.js +425 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.js +269 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js +366 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +108 -24
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +303 -180
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +11 -0
- package/src/tests/api_tests/calendar_events_bulk_update.test.ts +418 -0
- package/src/tests/api_tests/date_string_validation.test.ts +107 -0
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +138 -0
- package/src/tests/api_tests/field_redaction.test.ts +669 -0
- package/src/tests/api_tests/form_started_trigger.test.ts +1 -1
- package/src/tests/api_tests/form_submitted_trigger.test.ts +281 -0
- package/src/tests/api_tests/integrations_redacted.test.ts +245 -0
- package/src/tests/api_tests/mdb_sort.test.ts +259 -0
- package/src/tests/api_tests/openloop_webhooks.test.ts +64 -0
- package/src/tests/api_tests/organization_settings_duplicates.test.ts +201 -0
- package/src/tests/tests.ts +92 -6
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import { log_header, wait, async_test } from "@tellescope/testing"
|
|
5
|
+
import { Enduser } from "@tellescope/types-client"
|
|
6
|
+
import { setup_tests } from "../setup"
|
|
7
|
+
|
|
8
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
9
|
+
|
|
10
|
+
export const form_submitted_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
11
|
+
log_header("Form Submitted Trigger Tests (Multi-Form & Per-Form Conditions)")
|
|
12
|
+
|
|
13
|
+
// Setup: two forms with one string field each
|
|
14
|
+
const formA = await sdk.api.forms.createOne({ title: 'Form Submitted Trigger Test A' })
|
|
15
|
+
const fieldA = await sdk.api.form_fields.createOne({
|
|
16
|
+
formId: formA.id,
|
|
17
|
+
type: 'string',
|
|
18
|
+
title: 'FieldA',
|
|
19
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const formB = await sdk.api.forms.createOne({ title: 'Form Submitted Trigger Test B' })
|
|
23
|
+
const fieldB = await sdk.api.form_fields.createOne({
|
|
24
|
+
formId: formB.id,
|
|
25
|
+
type: 'string',
|
|
26
|
+
title: 'FieldB',
|
|
27
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Helper to prepare and submit a form response
|
|
31
|
+
const submitForm = async (formId: string, enduserId: string, responses: { fieldId: string, fieldTitle: string, answer: { type: 'string', value: string } }[]) => {
|
|
32
|
+
const { accessCode } = await sdk.api.form_responses.prepare_form_response({ enduserId, formId })
|
|
33
|
+
await sdk.api.form_responses.submit_form_response({ accessCode, responses })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Pre-create endusers for each scenario to avoid throttle
|
|
37
|
+
const enduserA1 = await sdk.api.endusers.createOne({ fname: 'fst-a1' })
|
|
38
|
+
const enduserB1 = await sdk.api.endusers.createOne({ fname: 'fst-b1' })
|
|
39
|
+
const enduserA2 = await sdk.api.endusers.createOne({ fname: 'fst-a2' })
|
|
40
|
+
const enduserB2 = await sdk.api.endusers.createOne({ fname: 'fst-b2' })
|
|
41
|
+
const enduserB3 = await sdk.api.endusers.createOne({ fname: 'fst-b3' })
|
|
42
|
+
const enduserA3 = await sdk.api.endusers.createOne({ fname: 'fst-a3' })
|
|
43
|
+
const enduserA4 = await sdk.api.endusers.createOne({ fname: 'fst-a4' })
|
|
44
|
+
|
|
45
|
+
const allEndusers = [enduserA1, enduserB1, enduserA2, enduserB2, enduserB3, enduserA3, enduserA4]
|
|
46
|
+
const allTriggers: { id: string }[] = []
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// ── Scenario 1: Backwards compatibility — global conditions on primary form ──
|
|
50
|
+
const trigger1 = await sdk.api.automation_triggers.createOne({
|
|
51
|
+
event: {
|
|
52
|
+
type: 'Form Submitted',
|
|
53
|
+
info: { formId: formA.id },
|
|
54
|
+
conditions: {
|
|
55
|
+
"$and": [{ "condition": { [fieldA.id]: "match-value" } }]
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
action: { type: 'Add Tags', info: { tags: ['global-cond-primary'] } },
|
|
59
|
+
status: 'Active',
|
|
60
|
+
title: "Scenario 1: Global conditions backwards compat",
|
|
61
|
+
})
|
|
62
|
+
allTriggers.push(trigger1)
|
|
63
|
+
|
|
64
|
+
await submitForm(formA.id, enduserA1.id, [{
|
|
65
|
+
fieldId: fieldA.id, fieldTitle: 'FieldA',
|
|
66
|
+
answer: { type: 'string', value: 'match-value' },
|
|
67
|
+
}])
|
|
68
|
+
await wait(undefined, 1000)
|
|
69
|
+
|
|
70
|
+
await async_test(
|
|
71
|
+
"Scenario 1: Global conditions on primary form still work (backwards compat)",
|
|
72
|
+
() => sdk.api.endusers.getOne(enduserA1.id),
|
|
73
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('global-cond-primary') }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await sdk.api.automation_triggers.deleteOne(trigger1.id)
|
|
77
|
+
|
|
78
|
+
// ── Scenario 2: Multi-form trigger fires on secondary form submission ──
|
|
79
|
+
const trigger2 = await sdk.api.automation_triggers.createOne({
|
|
80
|
+
event: {
|
|
81
|
+
type: 'Form Submitted',
|
|
82
|
+
info: { formId: formA.id, otherFormIds: [formB.id] },
|
|
83
|
+
},
|
|
84
|
+
action: { type: 'Add Tags', info: { tags: ['multi-form-secondary'] } },
|
|
85
|
+
status: 'Active',
|
|
86
|
+
title: "Scenario 2: Multi-form fires on secondary",
|
|
87
|
+
})
|
|
88
|
+
allTriggers.push(trigger2)
|
|
89
|
+
|
|
90
|
+
await submitForm(formB.id, enduserB1.id, [{
|
|
91
|
+
fieldId: fieldB.id, fieldTitle: 'FieldB',
|
|
92
|
+
answer: { type: 'string', value: 'anything' },
|
|
93
|
+
}])
|
|
94
|
+
await wait(undefined, 1000)
|
|
95
|
+
|
|
96
|
+
await async_test(
|
|
97
|
+
"Scenario 2: Trigger fires when secondary form (otherFormIds) is submitted",
|
|
98
|
+
() => sdk.api.endusers.getOne(enduserB1.id),
|
|
99
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('multi-form-secondary') }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await sdk.api.automation_triggers.deleteOne(trigger2.id)
|
|
103
|
+
|
|
104
|
+
// ── Scenario 3: Per-form conditions match on primary form ──
|
|
105
|
+
const trigger3 = await sdk.api.automation_triggers.createOne({
|
|
106
|
+
event: {
|
|
107
|
+
type: 'Form Submitted',
|
|
108
|
+
info: {
|
|
109
|
+
formId: formA.id,
|
|
110
|
+
otherFormIds: [formB.id],
|
|
111
|
+
conditionsByFormId: {
|
|
112
|
+
[formA.id]: { "$and": [{ "condition": { [fieldA.id]: "primary-match" } }] },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
action: { type: 'Add Tags', info: { tags: ['per-form-primary'] } },
|
|
117
|
+
status: 'Active',
|
|
118
|
+
title: "Scenario 3: Per-form conditions on primary",
|
|
119
|
+
})
|
|
120
|
+
allTriggers.push(trigger3)
|
|
121
|
+
|
|
122
|
+
await submitForm(formA.id, enduserA2.id, [{
|
|
123
|
+
fieldId: fieldA.id, fieldTitle: 'FieldA',
|
|
124
|
+
answer: { type: 'string', value: 'primary-match' },
|
|
125
|
+
}])
|
|
126
|
+
await wait(undefined, 1000)
|
|
127
|
+
|
|
128
|
+
await async_test(
|
|
129
|
+
"Scenario 3: Per-form conditions match on primary form",
|
|
130
|
+
() => sdk.api.endusers.getOne(enduserA2.id),
|
|
131
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('per-form-primary') }
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await sdk.api.automation_triggers.deleteOne(trigger3.id)
|
|
135
|
+
|
|
136
|
+
// ── Scenario 4: Per-form conditions match on secondary form ──
|
|
137
|
+
const trigger4 = await sdk.api.automation_triggers.createOne({
|
|
138
|
+
event: {
|
|
139
|
+
type: 'Form Submitted',
|
|
140
|
+
info: {
|
|
141
|
+
formId: formA.id,
|
|
142
|
+
otherFormIds: [formB.id],
|
|
143
|
+
conditionsByFormId: {
|
|
144
|
+
[formB.id]: { "$and": [{ "condition": { [fieldB.id]: "secondary-match" } }] },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
action: { type: 'Add Tags', info: { tags: ['per-form-secondary'] } },
|
|
149
|
+
status: 'Active',
|
|
150
|
+
title: "Scenario 4: Per-form conditions on secondary",
|
|
151
|
+
})
|
|
152
|
+
allTriggers.push(trigger4)
|
|
153
|
+
|
|
154
|
+
await submitForm(formB.id, enduserB2.id, [{
|
|
155
|
+
fieldId: fieldB.id, fieldTitle: 'FieldB',
|
|
156
|
+
answer: { type: 'string', value: 'secondary-match' },
|
|
157
|
+
}])
|
|
158
|
+
await wait(undefined, 1000)
|
|
159
|
+
|
|
160
|
+
await async_test(
|
|
161
|
+
"Scenario 4: Per-form conditions match on secondary form",
|
|
162
|
+
() => sdk.api.endusers.getOne(enduserB2.id),
|
|
163
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('per-form-secondary') }
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
await sdk.api.automation_triggers.deleteOne(trigger4.id)
|
|
167
|
+
|
|
168
|
+
// ── Scenario 5: Per-form conditions on one form don't block a different form ──
|
|
169
|
+
const trigger5 = await sdk.api.automation_triggers.createOne({
|
|
170
|
+
event: {
|
|
171
|
+
type: 'Form Submitted',
|
|
172
|
+
info: {
|
|
173
|
+
formId: formA.id,
|
|
174
|
+
otherFormIds: [formB.id],
|
|
175
|
+
conditionsByFormId: {
|
|
176
|
+
[formA.id]: { "$and": [{ "condition": { [fieldA.id]: "strict-value" } }] },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
action: { type: 'Add Tags', info: { tags: ['no-cross-block'] } },
|
|
181
|
+
status: 'Active',
|
|
182
|
+
title: "Scenario 5: No cross-form condition blocking",
|
|
183
|
+
})
|
|
184
|
+
allTriggers.push(trigger5)
|
|
185
|
+
|
|
186
|
+
await submitForm(formB.id, enduserB3.id, [{
|
|
187
|
+
fieldId: fieldB.id, fieldTitle: 'FieldB',
|
|
188
|
+
answer: { type: 'string', value: 'anything' },
|
|
189
|
+
}])
|
|
190
|
+
await wait(undefined, 1000)
|
|
191
|
+
|
|
192
|
+
await async_test(
|
|
193
|
+
"Scenario 5: Conditions on FormA do not block FormB (no cross-form blocking)",
|
|
194
|
+
() => sdk.api.endusers.getOne(enduserB3.id),
|
|
195
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('no-cross-block') }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
await sdk.api.automation_triggers.deleteOne(trigger5.id)
|
|
199
|
+
|
|
200
|
+
// ── Scenario 6: conditionsByFormId overrides global conditions ──
|
|
201
|
+
const trigger6 = await sdk.api.automation_triggers.createOne({
|
|
202
|
+
event: {
|
|
203
|
+
type: 'Form Submitted',
|
|
204
|
+
info: {
|
|
205
|
+
formId: formA.id,
|
|
206
|
+
conditionsByFormId: {
|
|
207
|
+
[formA.id]: { "$and": [{ "condition": { [fieldA.id]: "per-form-value" } }] },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
conditions: {
|
|
211
|
+
"$and": [{ "condition": { [fieldA.id]: "global-value" } }]
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
action: { type: 'Add Tags', info: { tags: ['override-test'] } },
|
|
215
|
+
status: 'Active',
|
|
216
|
+
title: "Scenario 6: conditionsByFormId overrides global",
|
|
217
|
+
})
|
|
218
|
+
allTriggers.push(trigger6)
|
|
219
|
+
|
|
220
|
+
// 6a: Submit with per-form matching value — should fire
|
|
221
|
+
await submitForm(formA.id, enduserA3.id, [{
|
|
222
|
+
fieldId: fieldA.id, fieldTitle: 'FieldA',
|
|
223
|
+
answer: { type: 'string', value: 'per-form-value' },
|
|
224
|
+
}])
|
|
225
|
+
await wait(undefined, 1000)
|
|
226
|
+
|
|
227
|
+
await async_test(
|
|
228
|
+
"Scenario 6a: conditionsByFormId match fires trigger (overrides global)",
|
|
229
|
+
() => sdk.api.endusers.getOne(enduserA3.id),
|
|
230
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('override-test') }
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// 6b: Submit with global matching value — should NOT fire (per-form takes precedence)
|
|
234
|
+
await submitForm(formA.id, enduserA4.id, [{
|
|
235
|
+
fieldId: fieldA.id, fieldTitle: 'FieldA',
|
|
236
|
+
answer: { type: 'string', value: 'global-value' },
|
|
237
|
+
}])
|
|
238
|
+
await wait(undefined, 1000)
|
|
239
|
+
|
|
240
|
+
await async_test(
|
|
241
|
+
"Scenario 6b: Global conditions ignored when conditionsByFormId exists",
|
|
242
|
+
() => sdk.api.endusers.getOne(enduserA4.id),
|
|
243
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('override-test') }
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await sdk.api.automation_triggers.deleteOne(trigger6.id)
|
|
247
|
+
|
|
248
|
+
} finally {
|
|
249
|
+
// Cleanup
|
|
250
|
+
for (const e of allEndusers) {
|
|
251
|
+
try { await sdk.api.endusers.deleteOne(e.id) } catch (err) { /* may already be deleted */ }
|
|
252
|
+
}
|
|
253
|
+
for (const t of allTriggers) {
|
|
254
|
+
try { await sdk.api.automation_triggers.deleteOne(t.id) } catch (err) { /* may already be deleted */ }
|
|
255
|
+
}
|
|
256
|
+
try { await sdk.api.forms.deleteOne(formA.id) } catch (err) { /* ignore */ }
|
|
257
|
+
try { await sdk.api.forms.deleteOne(formB.id) } catch (err) { /* ignore */ }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Allow running this test file independently
|
|
262
|
+
if (require.main === module) {
|
|
263
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
264
|
+
const sdk = new Session({ host })
|
|
265
|
+
const sdkNonAdmin = new Session({ host })
|
|
266
|
+
|
|
267
|
+
const runTests = async () => {
|
|
268
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
269
|
+
await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
runTests()
|
|
273
|
+
.then(() => {
|
|
274
|
+
console.log("✅ Form Submitted trigger tests completed successfully")
|
|
275
|
+
process.exit(0)
|
|
276
|
+
})
|
|
277
|
+
.catch((error) => {
|
|
278
|
+
console.error("❌ Form Submitted trigger tests failed:", error)
|
|
279
|
+
process.exit(1)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
log_header,
|
|
7
|
+
} from "@tellescope/testing"
|
|
8
|
+
import { setup_tests } from "../setup"
|
|
9
|
+
import { INTEGRATION_SENSITIVE_FIELDS } from "@tellescope/constants"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
const hasNoSensitiveFields = (integration: any) => {
|
|
14
|
+
for (const field of INTEGRATION_SENSITIVE_FIELDS) {
|
|
15
|
+
if (field in integration) return false
|
|
16
|
+
}
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const integrations_redacted_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
21
|
+
log_header("Integrations Redacted Endpoints Tests")
|
|
22
|
+
|
|
23
|
+
let integrationId = ''
|
|
24
|
+
let sensitiveIntegrationId = ''
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Create an integration with real auth data
|
|
28
|
+
const created = await sdk.api.integrations.createOne({
|
|
29
|
+
title: 'Test Redacted Integration',
|
|
30
|
+
authentication: { type: 'oauth2', info: { access_token: 'test-access-token', refresh_token: 'test-refresh-token', scope: '', token_type: 'Bearer', expiry_date: new Date().getTime() } },
|
|
31
|
+
webhooksSecret: 'super-secret-webhook',
|
|
32
|
+
emailDisabled: false,
|
|
33
|
+
})
|
|
34
|
+
integrationId = created.id
|
|
35
|
+
|
|
36
|
+
// load_redacted as creator — verify integration is returned, no sensitive fields present
|
|
37
|
+
await async_test(
|
|
38
|
+
"load_redacted as creator returns redacted integrations",
|
|
39
|
+
() => sdk.api.integrations.load_redacted({}),
|
|
40
|
+
{ onResult: r => {
|
|
41
|
+
const found = r.integrations.find((i: any) => i.id === integrationId)
|
|
42
|
+
return !!found && hasNoSensitiveFields(found) && found.title === 'Test Redacted Integration'
|
|
43
|
+
}}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// load_redacted as non-creator — verify same integration visible and redacted
|
|
47
|
+
await async_test(
|
|
48
|
+
"load_redacted as non-creator returns redacted integrations (bypasses CREATOR_ONLY)",
|
|
49
|
+
() => sdkNonAdmin.api.integrations.load_redacted({}),
|
|
50
|
+
{ onResult: r => {
|
|
51
|
+
const found = r.integrations.find((i: any) => i.id === integrationId)
|
|
52
|
+
return !!found && hasNoSensitiveFields(found) && found.title === 'Test Redacted Integration'
|
|
53
|
+
}}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Standard load endpoints enforce CREATOR_ONLY — creator gets full object, non-creator gets nothing
|
|
57
|
+
await async_test(
|
|
58
|
+
"getOne as creator returns full integration including sensitive fields",
|
|
59
|
+
() => sdk.api.integrations.getOne(integrationId),
|
|
60
|
+
{ onResult: i => !!i && 'authentication' in i && 'webhooksSecret' in i }
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await async_test(
|
|
64
|
+
"getOne as non-creator returns 404 (CREATOR_ONLY access control)",
|
|
65
|
+
() => sdkNonAdmin.api.integrations.getOne(integrationId),
|
|
66
|
+
{ shouldError: true, onError: () => true }
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await async_test(
|
|
70
|
+
"getSome as non-creator returns empty list (CREATOR_ONLY access control)",
|
|
71
|
+
() => sdkNonAdmin.api.integrations.getSome(),
|
|
72
|
+
{ onResult: r => Array.isArray(r) && r.find((i: any) => i.id === integrationId) === undefined }
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// update_settings as creator — update a non-sensitive field
|
|
76
|
+
await async_test(
|
|
77
|
+
"update_settings as creator updates non-sensitive field",
|
|
78
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { emailDisabled: true } }),
|
|
79
|
+
{ onResult: r => {
|
|
80
|
+
return !!r.integration && r.integration.emailDisabled === true && hasNoSensitiveFields(r.integration)
|
|
81
|
+
}}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// update_settings as non-creator — update another field
|
|
85
|
+
await async_test(
|
|
86
|
+
"update_settings as non-creator updates non-sensitive field",
|
|
87
|
+
() => sdkNonAdmin.api.integrations.update_settings({ id: integrationId, updates: { disableEnduserAutoSync: true } }),
|
|
88
|
+
{ onResult: r => {
|
|
89
|
+
return !!r.integration && r.integration.disableEnduserAutoSync === true && hasNoSensitiveFields(r.integration)
|
|
90
|
+
}}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// update_settings rejects sensitive fields
|
|
94
|
+
await async_test(
|
|
95
|
+
"update_settings rejects authentication update",
|
|
96
|
+
() => sdkNonAdmin.api.integrations.update_settings({ id: integrationId, updates: { authentication: { type: 'apiKey', info: { api_key: 'hacked' } } } as any }),
|
|
97
|
+
{ shouldError: true, onError: () => true }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
await async_test(
|
|
101
|
+
"update_settings rejects webhooksSecret update",
|
|
102
|
+
() => sdkNonAdmin.api.integrations.update_settings({ id: integrationId, updates: { webhooksSecret: 'hacked' } as any }),
|
|
103
|
+
{ shouldError: true, onError: () => true }
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
await async_test(
|
|
107
|
+
"update_settings rejects fhirClientSecret update",
|
|
108
|
+
() => sdkNonAdmin.api.integrations.update_settings({ id: integrationId, updates: { fhirClientSecret: 'hacked' } as any }),
|
|
109
|
+
{ shouldError: true, onError: () => true }
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Group 1: Dot-notation bypass tests (validates the allowlist fix)
|
|
113
|
+
await async_test(
|
|
114
|
+
"update_settings rejects dot-notation authentication.info.api_key update",
|
|
115
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { "authentication.info.api_key": "hacked" } as any }),
|
|
116
|
+
{ shouldError: true, onError: () => true }
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
await async_test(
|
|
120
|
+
"update_settings rejects dot-notation authentication.type update",
|
|
121
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { "authentication.type": "hacked" } as any }),
|
|
122
|
+
{ shouldError: true, onError: () => true }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Group 2: Remaining sensitive field rejections
|
|
126
|
+
await async_test(
|
|
127
|
+
"update_settings rejects fhirClientId update",
|
|
128
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { fhirClientId: 'hacked' } as any }),
|
|
129
|
+
{ shouldError: true, onError: () => true }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await async_test(
|
|
133
|
+
"update_settings rejects fhirAccessToken update",
|
|
134
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { fhirAccessToken: 'hacked' } as any }),
|
|
135
|
+
{ shouldError: true, onError: () => true }
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Group 3: Non-allowlisted field rejections
|
|
139
|
+
await async_test(
|
|
140
|
+
"update_settings rejects title update (not in allowlist)",
|
|
141
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { title: 'hacked' } as any }),
|
|
142
|
+
{ shouldError: true, onError: () => true }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
await async_test(
|
|
146
|
+
"update_settings rejects environment update (not in allowlist)",
|
|
147
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { environment: 'hacked' } as any }),
|
|
148
|
+
{ shouldError: true, onError: () => true }
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
await async_test(
|
|
152
|
+
"update_settings rejects unknown field update",
|
|
153
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { unknownField: 'hacked' } as any }),
|
|
154
|
+
{ shouldError: true, onError: () => true }
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
// Group 4: Verify additional allowlisted fields can be updated
|
|
158
|
+
await async_test(
|
|
159
|
+
"update_settings allows syncUnrecognizedSenders update",
|
|
160
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { syncUnrecognizedSenders: true } }),
|
|
161
|
+
{ onResult: r => !!r.integration && r.integration.syncUnrecognizedSenders === true && hasNoSensitiveFields(r.integration) }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
await async_test(
|
|
165
|
+
"update_settings allows pushCalendarDetails update",
|
|
166
|
+
() => sdk.api.integrations.update_settings({ id: integrationId, updates: { pushCalendarDetails: true } }),
|
|
167
|
+
{ onResult: r => !!r.integration && r.integration.pushCalendarDetails === true && hasNoSensitiveFields(r.integration) }
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Group 5: Explicit per-field redaction verification
|
|
171
|
+
// Note: fhirClientId/fhirClientSecret/fhirAccessToken are set server-side via OAuth flows only,
|
|
172
|
+
// so we test redaction of the fields that can be set via createOne (authentication, webhooksSecret)
|
|
173
|
+
const sensitiveIntegration = await sdk.api.integrations.createOne({
|
|
174
|
+
title: 'Test All Sensitive Fields',
|
|
175
|
+
authentication: { type: 'oauth2', info: { access_token: 'secret-access-token', refresh_token: 'secret-refresh-token', scope: '', token_type: 'Bearer', expiry_date: new Date().getTime() } },
|
|
176
|
+
webhooksSecret: 'super-secret-webhook-456',
|
|
177
|
+
emailDisabled: true,
|
|
178
|
+
})
|
|
179
|
+
sensitiveIntegrationId = sensitiveIntegration.id
|
|
180
|
+
|
|
181
|
+
await async_test(
|
|
182
|
+
"load_redacted omits authentication field individually",
|
|
183
|
+
() => sdk.api.integrations.load_redacted({}),
|
|
184
|
+
{ onResult: r => {
|
|
185
|
+
const found = r.integrations.find((i: any) => i.id === sensitiveIntegrationId)
|
|
186
|
+
return !!found && !('authentication' in found)
|
|
187
|
+
}}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
await async_test(
|
|
191
|
+
"load_redacted omits webhooksSecret field individually",
|
|
192
|
+
() => sdk.api.integrations.load_redacted({}),
|
|
193
|
+
{ onResult: r => {
|
|
194
|
+
const found = r.integrations.find((i: any) => i.id === sensitiveIntegrationId)
|
|
195
|
+
return !!found && !('webhooksSecret' in found)
|
|
196
|
+
}}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
await async_test(
|
|
200
|
+
"load_redacted still returns non-sensitive fields",
|
|
201
|
+
() => sdk.api.integrations.load_redacted({}),
|
|
202
|
+
{ onResult: r => {
|
|
203
|
+
const found = r.integrations.find((i: any) => i.id === sensitiveIntegrationId)
|
|
204
|
+
return !!found && found.emailDisabled === true && found.title === 'Test All Sensitive Fields'
|
|
205
|
+
}}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
} finally {
|
|
209
|
+
if (integrationId) {
|
|
210
|
+
try {
|
|
211
|
+
await sdk.api.integrations.deleteOne(integrationId)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('Cleanup error:', error)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (sensitiveIntegrationId) {
|
|
217
|
+
try {
|
|
218
|
+
await sdk.api.integrations.deleteOne(sensitiveIntegrationId)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Cleanup error (sensitive integration):', error)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (require.main === module) {
|
|
227
|
+
console.log(`Using API URL: ${host}`)
|
|
228
|
+
const sdk = new Session({ host })
|
|
229
|
+
const sdkNonAdmin = new Session({ host })
|
|
230
|
+
|
|
231
|
+
const runTests = async () => {
|
|
232
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
233
|
+
await integrations_redacted_tests({ sdk, sdkNonAdmin })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
runTests()
|
|
237
|
+
.then(() => {
|
|
238
|
+
console.log("Integrations redacted test suite completed successfully")
|
|
239
|
+
process.exit(0)
|
|
240
|
+
})
|
|
241
|
+
.catch((error) => {
|
|
242
|
+
console.error("Integrations redacted test suite failed:", error)
|
|
243
|
+
process.exit(1)
|
|
244
|
+
})
|
|
245
|
+
}
|