@tellescope/sdk 1.255.5 → 1.255.7

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.
Files changed (36) hide show
  1. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
  2. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js +66 -37
  3. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/journey_delete_cancels_actions.test.d.ts +6 -0
  5. package/lib/cjs/tests/api_tests/journey_delete_cancels_actions.test.d.ts.map +1 -0
  6. package/lib/cjs/tests/api_tests/journey_delete_cancels_actions.test.js +250 -0
  7. package/lib/cjs/tests/api_tests/journey_delete_cancels_actions.test.js.map +1 -0
  8. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.d.ts +36 -0
  9. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.d.ts.map +1 -0
  10. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +203 -0
  11. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -0
  12. package/lib/cjs/tests/tests.d.ts.map +1 -1
  13. package/lib/cjs/tests/tests.js +174 -166
  14. package/lib/cjs/tests/tests.js.map +1 -1
  15. package/lib/esm/sdk.d.ts +2 -2
  16. package/lib/esm/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
  17. package/lib/esm/tests/api_tests/healthie_multi_integration.test.js +67 -38
  18. package/lib/esm/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
  19. package/lib/esm/tests/api_tests/journey_delete_cancels_actions.test.d.ts +6 -0
  20. package/lib/esm/tests/api_tests/journey_delete_cancels_actions.test.d.ts.map +1 -0
  21. package/lib/esm/tests/api_tests/journey_delete_cancels_actions.test.js +223 -0
  22. package/lib/esm/tests/api_tests/journey_delete_cancels_actions.test.js.map +1 -0
  23. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.d.ts +36 -0
  24. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.d.ts.map +1 -0
  25. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +196 -0
  26. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -0
  27. package/lib/esm/tests/tests.d.ts.map +1 -1
  28. package/lib/esm/tests/tests.js +174 -166
  29. package/lib/esm/tests/tests.js.map +1 -1
  30. package/lib/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +6 -6
  32. package/src/tests/api_tests/healthie_multi_integration.test.ts +56 -15
  33. package/src/tests/api_tests/journey_delete_cancels_actions.test.ts +152 -0
  34. package/src/tests/api_tests/security/register_business_id_injection.test.ts +140 -0
  35. package/src/tests/tests.ts +4 -0
  36. package/test_generated.pdf +0 -0
@@ -3,10 +3,12 @@ require('source-map-support').install();
3
3
  import axios from "axios"
4
4
  import { Session } from "../../sdk"
5
5
  import {
6
+ assert,
6
7
  async_test,
7
8
  log_header,
8
9
  wait,
9
10
  } from "@tellescope/testing"
11
+ import { evaluate_conditional_logic_for_enduser_fields } from "@tellescope/utilities"
10
12
  import { setup_tests } from "../setup"
11
13
  import { BUILT_INS_FOR_SET_FIELDS, HEALTHIE_TITLE } from "@tellescope/constants"
12
14
 
@@ -33,6 +35,43 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
33
35
  }
34
36
  console.log("✅ healthieIntegrationId exposed in BUILT_INS_FOR_SET_FIELDS")
35
37
 
38
+ // ── Conditional logic evaluation (pure — resolves via the generic built-in field fallback) ──
39
+ const conditionPlaceholders = {
40
+ businessId: '', creator: '', hashedPassword: '', lastActive: '', lastLogout: '', updatedAt: new Date(),
41
+ }
42
+ const taggedForConditions = { ...conditionPlaceholders, healthieIntegrationId: TAG } as any
43
+ const untaggedForConditions = { ...conditionPlaceholders } as any
44
+
45
+ assert(
46
+ evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: TAG } }] }),
47
+ 'Conditional logic error', 'healthieIntegrationId equality matches a tagged patient',
48
+ )
49
+ assert(
50
+ !evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: 'other-clinic' } }] }),
51
+ 'Conditional logic error', 'healthieIntegrationId equality rejects a different id',
52
+ )
53
+ assert(
54
+ !evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: TAG } }] }),
55
+ 'Conditional logic error', 'healthieIntegrationId equality rejects an unset (primary) patient',
56
+ )
57
+ assert(
58
+ evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $isSet: true } } }] }),
59
+ 'Conditional logic error', 'healthieIntegrationId $isSet matches a tagged patient',
60
+ )
61
+ assert(
62
+ evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $isNotSet: true } } }] }),
63
+ 'Conditional logic error', 'healthieIntegrationId $isNotSet matches an unset (primary) patient',
64
+ )
65
+ assert(
66
+ !evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $ne: TAG } } }] }),
67
+ 'Conditional logic error', 'healthieIntegrationId $ne rejects the matching tag',
68
+ )
69
+ assert(
70
+ evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $ne: TAG } } }] }),
71
+ 'Conditional logic error', 'healthieIntegrationId $ne matches an unset (primary) patient',
72
+ )
73
+ console.log("✅ healthieIntegrationId evaluates in conditional logic (equality, $isSet/$isNotSet, $ne)")
74
+
36
75
  // ── Create primary (untagged, sandbox-style key) + additional (tagged, production-style key)
37
76
  // via the standard REST create endpoint — the real creation path for additional connections ──
38
77
  const primary = await sdk.api.integrations.createOne({
@@ -80,6 +119,15 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
80
119
  { onResult: i => !i.pushAddedTags },
81
120
  )
82
121
 
122
+ // ── Invalid API key surfaces as an actionable 400 (fake key → Healthie rejects the request) ──
123
+ await async_test(
124
+ "proxy_read webhooks with an invalid API key returns a clear error",
125
+ () => sdk.api.integrations.proxy_read({
126
+ integration: HEALTHIE_TITLE, type: 'webhooks', healthieIntegrationId: TAG,
127
+ }),
128
+ { shouldError: true, onError: (e: any) => (e?.message || '').includes('API Key is Invalid') },
129
+ )
130
+
83
131
  // ── Organization.healthieIntegrationIds pushed by create side effect ──
84
132
  await wait(undefined, 2000) // side effects are async
85
133
  await async_test(
@@ -129,21 +177,21 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
129
177
  // link the patient to Healthie (reference with a Healthie patient id)
130
178
  await sdk.api.endusers.updateOne(enduserId, { references: [{ type: HEALTHIE_TITLE, id: '12345' }] } as any, { replaceObjectFields: true })
131
179
 
180
+ // no lock: linked patients stay editable so customers can self-serve fix or migrate them
132
181
  await async_test(
133
- "healthieIntegrationId locked once a Healthie patient ID exists",
182
+ "healthieIntegrationId changeable while linked (self-serve fix/migrate)",
134
183
  () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: 'somewhere-else' }),
135
- { shouldError: true, onError: (e: any) => (e?.message || e?.toString() || '').includes('healthieIntegrationId') },
184
+ { onResult: e => e.healthieIntegrationId === 'somewhere-else' },
136
185
  )
137
186
  await async_test(
138
- "no-op write of the same value still allowed while locked",
187
+ "healthieIntegrationId revertible while linked",
139
188
  () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: TAG }),
140
189
  { onResult: e => e.healthieIntegrationId === TAG },
141
190
  )
142
191
 
143
- // unlink → changeable again
144
192
  await sdk.api.endusers.updateOne(enduserId, { references: [] } as any, { replaceObjectFields: true })
145
193
  await async_test(
146
- "healthieIntegrationId changeable again after unlinking",
194
+ "healthieIntegrationId clearable",
147
195
  () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: '' }),
148
196
  { onResult: e => !e.healthieIntegrationId },
149
197
  )
@@ -174,7 +222,7 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
174
222
  }},
175
223
  )
176
224
 
177
- // link the formsort enduser, then attempt a relink via formsort field change must be dropped
225
+ // linked endusers are re-routable via formsort too (self-serve fix/migrate)
178
226
  await sdk.api.endusers.updateOne(formsortEnduserId, { references: [{ type: HEALTHIE_TITLE, id: '6789' }] } as any, { replaceObjectFields: true })
179
227
  await postToFormsort([
180
228
  { key: 'email', value: formsortEmail },
@@ -183,16 +231,9 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
183
231
  await wait(undefined, 1000)
184
232
 
185
233
  await async_test(
186
- "formsort relink attempt on linked enduser is dropped",
234
+ "formsort updates healthieIntegrationId on a linked enduser",
187
235
  () => sdk.api.endusers.getOne(formsortEnduserId),
188
- { onResult: e => e.healthieIntegrationId === TAG },
189
- )
190
- await async_test(
191
- "formsort relink attempt logs a customer-visible background error",
192
- () => sdk.api.background_errors.getSome(),
193
- { onResult: errors => errors.some(e =>
194
- e.title === "Healthie Integration Change Blocked" && e.enduserId === formsortEnduserId
195
- )},
236
+ { onResult: e => e.healthieIntegrationId === 'somewhere-else' },
196
237
  )
197
238
 
198
239
  // ── Removing the primary must not affect the tagged integration; org list recomputes on delete ──
@@ -0,0 +1,152 @@
1
+ import { Session } from '../../sdk'
2
+ import { log_header, wait } from "@tellescope/testing"
3
+ import { AUTOMATED_ACTION_CANCEL_REASONS } from "@tellescope/constants"
4
+
5
+ const host = process.env.API_URL || 'http://localhost:8080'
6
+
7
+ export const journey_delete_cancels_actions_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
8
+ log_header("Journey Delete Cancels Pending Actions Tests")
9
+
10
+ let journeyId: string | undefined
11
+ let enduserId: string | undefined
12
+ let stepId: string | undefined
13
+
14
+ try {
15
+ // Create a journey + an automation_step
16
+ const journey = await sdk.api.journeys.createOne({ title: "Cascade Cancel Journey " + Date.now() })
17
+ journeyId = journey.id
18
+
19
+ const step = await sdk.api.automation_steps.createOne({
20
+ journeyId: journey.id,
21
+ action: { type: 'setEnduserStatus', info: { status: 'Test Status' } },
22
+ events: [{ type: 'onJourneyStart', info: {} }],
23
+ })
24
+ stepId = step.id
25
+
26
+ // Create an enduser
27
+ const enduser = await sdk.api.endusers.createOne({
28
+ fname: 'Cascade',
29
+ lname: 'Cancel',
30
+ email: 'test-cascade-cancel-' + Date.now() + '@example.com',
31
+ })
32
+ enduserId = enduser.id
33
+
34
+ // Create an active action with a far-future processAfter so the worker won't process it during the test
35
+ const activeAction = await sdk.api.automated_actions.createOne({
36
+ journeyId: journey.id,
37
+ automationStepId: step.id,
38
+ enduserId: enduser.id,
39
+ processAfter: Date.now() + 1000000, // far future so the worker won't process it
40
+ status: 'active',
41
+ event: { type: 'onJourneyStart', info: {} },
42
+ action: { type: 'setEnduserStatus', info: { status: 'Test Status' } },
43
+ })
44
+
45
+ // Create a finished action to confirm it is left untouched by the cascade
46
+ const finishedAction = await sdk.api.automated_actions.createOne({
47
+ journeyId: journey.id,
48
+ automationStepId: step.id,
49
+ enduserId: enduser.id,
50
+ processAfter: Date.now() + 1000000,
51
+ status: 'finished',
52
+ event: { type: 'onJourneyStart', info: {} },
53
+ action: { type: 'setEnduserStatus', info: { status: 'Test Status' } },
54
+ })
55
+
56
+ // Assert the action starts active
57
+ const before = await sdk.api.automated_actions.getOne(activeAction.id)
58
+ if (before.status !== 'active') {
59
+ throw new Error(`Expected action to start 'active', got '${before.status}'`)
60
+ }
61
+ console.log("✓ Action is active before journey deletion")
62
+
63
+ // Delete the journey — this should cascade cancel the pending action
64
+ await sdk.api.journeys.deleteOne(journey.id)
65
+ journeyId = undefined // already deleted, skip in cleanup
66
+
67
+ // Poll until the active action transitions to cancelled with the Journey Deleted reason,
68
+ // attributed to the deleting user via cancelledBy
69
+ const expectedReason = AUTOMATED_ACTION_CANCEL_REASONS.indexOf('Journey Deleted')
70
+ await pollForResults(
71
+ async () => {
72
+ const actions = await sdk.api.automated_actions.getSome({ filter: { enduserId: enduser.id } })
73
+ const active = actions.find(a => a.id === activeAction.id)
74
+ const finished = actions.find(a => a.id === finishedAction.id)
75
+ return { active, finished }
76
+ },
77
+ ({ active }) => (
78
+ active?.status === 'cancelled'
79
+ && active?.cancelReason === expectedReason
80
+ && active?.cancelledBy === sdk.userInfo.id
81
+ ),
82
+ 'Active action should be cancelled with Journey Deleted reason and cancelledBy after journey deletion',
83
+ )
84
+ console.log("✓ Active action cancelled with Journey Deleted reason and cancelledBy set to deleting user")
85
+
86
+ // Not covered here: journey deletion also removes outstanding_froms_trackers. Creating a tracker
87
+ // requires an automation to actually send an email/SMS with multiple form links, and the collection
88
+ // is internal-only (no API read access), so it can't be verified from an SDK test.
89
+
90
+ // Confirm the finished action was left untouched
91
+ const finishedAfter = await sdk.api.automated_actions.getOne(finishedAction.id)
92
+ if (finishedAfter.status !== 'finished') {
93
+ throw new Error(`Expected finished action to remain 'finished', got '${finishedAfter.status}'`)
94
+ }
95
+ console.log("✓ Finished action left untouched")
96
+
97
+ console.log("✅ Journey Delete Cancels Pending Actions tests completed successfully")
98
+ } finally {
99
+ // Cleanup — journey is already deleted if the flow completed
100
+ await Promise.all([
101
+ enduserId ? sdk.api.endusers.deleteOne(enduserId).catch(() => {}) : undefined,
102
+ stepId ? sdk.api.automation_steps.deleteOne(stepId).catch(() => {}) : undefined,
103
+ journeyId ? sdk.api.journeys.deleteOne(journeyId).catch(() => {}) : undefined,
104
+ ])
105
+ }
106
+ }
107
+
108
+ const pollForResults = async <T>(
109
+ fetchFn: () => Promise<T>,
110
+ evaluateFn: (result: T) => boolean,
111
+ description: string,
112
+ intervalMs = 500,
113
+ maxIterations = 30,
114
+ ): Promise<void> => {
115
+ let lastResult: T | undefined
116
+
117
+ for (let i = 0; i < maxIterations; i++) {
118
+ await wait(undefined, intervalMs)
119
+
120
+ lastResult = await fetchFn()
121
+ if (evaluateFn(lastResult)) {
122
+ console.log(`✓ ${description} - completed after ${(i + 1) * intervalMs}ms`)
123
+ return
124
+ }
125
+ }
126
+
127
+ console.log('Final polling result:', lastResult)
128
+ throw new Error(`Polling timeout: ${description} - waited ${maxIterations * intervalMs}ms`)
129
+ }
130
+
131
+ // Allow running this test file independently
132
+ if (require.main === module) {
133
+ console.log(`🌐 Using API URL: ${host}`)
134
+ const sdk = new Session({ host })
135
+ const sdkNonAdmin = new Session({ host })
136
+
137
+ const runTests = async () => {
138
+ const { setup_tests } = await import("../setup")
139
+ await setup_tests(sdk, sdkNonAdmin)
140
+ await journey_delete_cancels_actions_tests({ sdk, sdkNonAdmin })
141
+ }
142
+
143
+ runTests()
144
+ .then(() => {
145
+ console.log("✅ Journey Delete Cancels Pending Actions test suite completed successfully")
146
+ process.exit(0)
147
+ })
148
+ .catch((error) => {
149
+ console.error("❌ Journey Delete Cancels Pending Actions test suite failed:", error)
150
+ process.exit(1)
151
+ })
152
+ }
@@ -0,0 +1,140 @@
1
+ require('source-map-support').install();
2
+
3
+ import axios from "axios"
4
+ import { Session } from "../../../sdk"
5
+ import {
6
+ assert,
7
+ log_header,
8
+ wait,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ const post = async (path: string, body: any, headers: Record<string, string> = {}) => {
15
+ try {
16
+ const res = await axios.post(`${host}${path}`, body, {
17
+ validateStatus: () => true,
18
+ headers,
19
+ })
20
+ return { status: res.status, data: res.data }
21
+ } catch (err: any) {
22
+ return { status: err?.response?.status, data: err?.response?.data }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Reproduction + regression test for the reported vulnerability:
28
+ *
29
+ * A @wearehackerone.com email "joined" a customer's organization without being invited, and
30
+ * duplicate user records existed for that email — each bound to a different businessId.
31
+ *
32
+ * Root cause: the public `POST /v1/users/register` endpoint. `addPublicEndpoint` accepts an
33
+ * OPTIONAL, caller-supplied `businessId` (routing.ts) and bakes it into the handler's DB. The
34
+ * register handler's only duplicate guard (`DB.users.findOne({ email })`) is therefore scoped to
35
+ * whatever org the caller named, and the subsequent `DB.users.insertOne(...)` stamps that
36
+ * caller-supplied businessId onto a new `accountType: 'Business'` user — bypassing the
37
+ * globalUnique: ['email'] enforcement that only runs in the generic create route.
38
+ *
39
+ * The "victim org" here is the test admin's own org (sdk.userInfo.businessId): an org the
40
+ * anonymous caller was never invited to. The admin is used purely to OBSERVE whether an
41
+ * uninvited user record materialized inside their org (getOne({ email }) is org-scoped and
42
+ * throws when the email is absent from the caller's tenant).
43
+ *
44
+ * Expected results:
45
+ * - VULNERABLE code: the assertions FAIL — the planted user appears inside the victim org,
46
+ * proving the exploit ("EXPLOIT CONFIRMED" in the failure message).
47
+ * - FIXED code: register ignores the caller-supplied businessId (self-signups are created
48
+ * org-less and duplicates are rejected globally), so nothing appears in the victim org and
49
+ * the assertions PASS.
50
+ *
51
+ * Uses unique emails per run (Date.now()) so re-runs never collide on the platform-wide unique
52
+ * email constraint. NOTE: on the FIXED path these registrations create org-less user records
53
+ * that an org-scoped admin cannot delete — harmless residue (unique emails), inherent to the
54
+ * legitimate "root register, no org" signup behavior.
55
+ */
56
+ export const register_business_id_injection_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
57
+ log_header("register businessId injection")
58
+
59
+ const victimBusinessId = sdk.userInfo.businessId
60
+ const password = 'testpassword123!'
61
+ const termsVersion = '1'
62
+
63
+ // ==================================================================================
64
+ // Scenario 1 — uninvited org join
65
+ // An anonymous caller registers with businessId set to the victim org. On vulnerable
66
+ // code this creates a Business user INSIDE the victim org that no one invited.
67
+ // ==================================================================================
68
+ const email1 = `security+reg-inject-${Date.now()}@wearehackerone.com`
69
+
70
+ const before = await sdk.api.users.getOne({ email: email1 }).catch(() => null)
71
+ assert(before === null, `precondition failed: ${email1} already present in victim org`, 'precondition: victim org has no such user')
72
+
73
+ const res1 = await post('/v1/users/register', { email: email1, password, termsVersion, businessId: victimBusinessId })
74
+ console.log(` register(businessId=victim) -> HTTP ${res1.status}`)
75
+
76
+ const planted1: any = await sdk.api.users.getOne({ email: email1 }).catch(() => null)
77
+ const injectedIntoVictimOrg = !!planted1 && planted1.businessId === victimBusinessId
78
+
79
+ // Clean up BEFORE asserting (a failed assert calls process.exit(1) and would skip cleanup).
80
+ if (planted1?.id) await sdk.api.users.deleteOne(planted1.id).catch(() => {})
81
+
82
+ assert(
83
+ !injectedIntoVictimOrg,
84
+ `❌ EXPLOIT CONFIRMED: anonymous POST /v1/users/register with businessId=${victimBusinessId} created user `
85
+ + `${email1} (accountType=${planted1?.accountType}) inside the victim org — no invite required`,
86
+ '✅ register ignores caller-supplied businessId — no user injected into victim org',
87
+ )
88
+
89
+ await wait(undefined, 1200) // register endpoint rate-limits ~1/sec per IP
90
+
91
+ // ==================================================================================
92
+ // Scenario 2 — cross-org duplicate for the same email
93
+ // First register with NO businessId (the legitimate "root register, no org" record —
94
+ // the "oldest" account in the report). Then register the SAME email WITH the victim
95
+ // businessId. On vulnerable code the scoped duplicate check misses the org-less record,
96
+ // so a SECOND user record for the same email is created inside the victim org.
97
+ // ==================================================================================
98
+ const email2 = `security+reg-dup-${Date.now()}@wearehackerone.com`
99
+
100
+ const resRoot = await post('/v1/users/register', { email: email2, password, termsVersion })
101
+ console.log(` register(no businessId) -> HTTP ${resRoot.status}`)
102
+
103
+ await wait(undefined, 1200)
104
+
105
+ const resDup = await post('/v1/users/register', { email: email2, password, termsVersion, businessId: victimBusinessId })
106
+ console.log(` register(same email, businessId=victim) -> HTTP ${resDup.status}`)
107
+
108
+ const planted2: any = await sdk.api.users.getOne({ email: email2 }).catch(() => null)
109
+ const duplicateInVictimOrg = !!planted2 && planted2.businessId === victimBusinessId
110
+
111
+ if (planted2?.id) await sdk.api.users.deleteOne(planted2.id).catch(() => {})
112
+
113
+ assert(
114
+ !duplicateInVictimOrg,
115
+ `❌ EXPLOIT CONFIRMED: same email ${email2} registered into a second org — duplicate user `
116
+ + `(accountType=${planted2?.accountType}) created in victim org ${victimBusinessId}`,
117
+ '✅ register enforces global email uniqueness — no cross-org duplicate created',
118
+ )
119
+ }
120
+
121
+ // Allow running this test file independently
122
+ if (require.main === module) {
123
+ const sdk = new Session({ host })
124
+ const sdkNonAdmin = new Session({ host })
125
+
126
+ const runTests = async () => {
127
+ await setup_tests(sdk, sdkNonAdmin)
128
+ await register_business_id_injection_tests({ sdk, sdkNonAdmin })
129
+ }
130
+
131
+ runTests()
132
+ .then(() => {
133
+ console.log("✅ register businessId injection test suite completed successfully")
134
+ process.exit(0)
135
+ })
136
+ .catch((error) => {
137
+ console.error("❌ register businessId injection test suite failed:", error)
138
+ process.exit(1)
139
+ })
140
+ }
@@ -53,6 +53,7 @@ import { appointment_rescheduled_trigger_tests } from "./api_tests/appointment_r
53
53
  import { appointment_no_showed_trigger_tests } from "./api_tests/appointment_no_showed_trigger.test"
54
54
  import { group_event_attendee_status_triggers_tests } from "./api_tests/group_event_attendee_status_triggers.test"
55
55
  import { journey_error_branching_tests } from "./api_tests/journey_error_branching.test"
56
+ import { journey_delete_cancels_actions_tests } from "./api_tests/journey_delete_cancels_actions.test"
56
57
  import { afteraction_day_of_month_delay_tests } from "./api_tests/afteraction_day_of_month_delay.test"
57
58
  import { push_forms_to_portal_group_completion_tests } from "./api_tests/push_forms_to_portal_group_completion.test"
58
59
  import { setup_tests } from "./setup"
@@ -102,6 +103,7 @@ import { ai_conversations_rbac_tests } from "./api_tests/security/F-0005-ai-conv
102
103
  import { cascade_role_rename_cross_tenant_tests } from "./api_tests/security/F-0053-cascade-role-rename-cross-tenant.test";
103
104
  import { self_admin_role_assignment_tests } from "./api_tests/security/F-0076-self-admin-role-assignment.test";
104
105
  import { invite_user_enumeration_tests } from "./api_tests/security/F-0007-invite-user-enumeration.test";
106
+ import { register_business_id_injection_tests } from "./api_tests/security/register_business_id_injection.test";
105
107
  import { handle_incoming_communication_cross_tenant_tests } from "./api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test";
106
108
  import { sanitize_user_html_xss_tests } from "./api_tests/security/F-0013-sanitize-user-html.test";
107
109
  import { prototype_pollution_tests } from "./api_tests/security/F-0016-prototype-pollution.test";
@@ -15036,6 +15038,7 @@ const ip_address_form_tests = async () => {
15036
15038
  await replace_form_field_template_values_tests()
15037
15039
  await mfa_tests()
15038
15040
  await setup_tests(sdk, sdkNonAdmin)
15041
+ await register_business_id_injection_tests({ sdk, sdkNonAdmin })
15039
15042
  await automation_trigger_tests()
15040
15043
  await resource_access_tags_tests({ sdk, sdkNonAdmin })
15041
15044
  await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
@@ -15111,6 +15114,7 @@ const ip_address_form_tests = async () => {
15111
15114
  await test_ticket_automation_assignment_and_optimization()
15112
15115
  await monthly_availability_restrictions_tests({ sdk, sdkNonAdmin })
15113
15116
  await journey_error_branching_tests({ sdk, sdkNonAdmin })
15117
+ await journey_delete_cancels_actions_tests({ sdk, sdkNonAdmin })
15114
15118
  await message_assignment_trigger_tests({ sdk })
15115
15119
  await inbox_threads_building_tests()
15116
15120
  await inbox_threads_loading_tests()
Binary file