@tellescope/sdk 1.252.2 → 1.253.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.
Files changed (40) hide show
  1. package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  2. package/lib/cjs/tests/api_tests/integrations_redacted.test.js +10 -4
  3. package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/mdi_webhooks.test.d.ts +3 -4
  5. package/lib/cjs/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/mdi_webhooks.test.js +3 -4
  7. package/lib/cjs/tests/api_tests/mdi_webhooks.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/no_access_permission_checks.test.d.ts.map +1 -1
  9. package/lib/cjs/tests/api_tests/no_access_permission_checks.test.js +108 -62
  10. package/lib/cjs/tests/api_tests/no_access_permission_checks.test.js.map +1 -1
  11. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  12. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +177 -0
  13. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  14. package/lib/cjs/tests/tests.d.ts.map +1 -1
  15. package/lib/cjs/tests/tests.js +131 -127
  16. package/lib/cjs/tests/tests.js.map +1 -1
  17. package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  18. package/lib/esm/tests/api_tests/integrations_redacted.test.js +10 -4
  19. package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -1
  20. package/lib/esm/tests/api_tests/mdi_webhooks.test.d.ts +3 -4
  21. package/lib/esm/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
  22. package/lib/esm/tests/api_tests/mdi_webhooks.test.js +3 -4
  23. package/lib/esm/tests/api_tests/mdi_webhooks.test.js.map +1 -1
  24. package/lib/esm/tests/api_tests/no_access_permission_checks.test.d.ts.map +1 -1
  25. package/lib/esm/tests/api_tests/no_access_permission_checks.test.js +108 -62
  26. package/lib/esm/tests/api_tests/no_access_permission_checks.test.js.map +1 -1
  27. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  28. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +177 -0
  29. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  30. package/lib/esm/tests/tests.d.ts.map +1 -1
  31. package/lib/esm/tests/tests.js +131 -127
  32. package/lib/esm/tests/tests.js.map +1 -1
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +10 -10
  35. package/src/tests/api_tests/integrations_redacted.test.ts +5 -3
  36. package/src/tests/api_tests/mdi_webhooks.test.ts +99 -0
  37. package/src/tests/api_tests/no_access_permission_checks.test.ts +38 -0
  38. package/src/tests/api_tests/set_fields_order_templates.test.ts +122 -0
  39. package/src/tests/tests.ts +2 -0
  40. package/test_generated.pdf +0 -0
@@ -0,0 +1,99 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ assert,
6
+ log_header,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../setup"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const mdiUrl = `${host}/v1/webhooks/mdi`
13
+
14
+ /**
15
+ * POST a JSON payload to the MDI webhook endpoint with optional auth/signature headers.
16
+ */
17
+ const postMDI = async (body: object, headers: Record<string, string> = {}) => {
18
+ const res = await fetch(mdiUrl, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json', ...headers },
21
+ body: JSON.stringify(body),
22
+ })
23
+ let data: any
24
+ try { data = await res.text() } catch { data = null }
25
+ return { status: res.status, data }
26
+ }
27
+
28
+ /**
29
+ * MD Integrations Webhooks Tests
30
+ *
31
+ * The inbound endpoint is security-first: it authenticates the tenant via the
32
+ * static Authorization token BEFORE any DB write or side effect. These tests
33
+ * assert those rejection paths, which don't require a configured MDI integration.
34
+ *
35
+ * NOTE: a full event round-trip (case_approved -> mdiStatus, offering_submitted
36
+ * -> enduser_medications, message_created -> chat, etc.) requires an MDI
37
+ * integration row carrying the Authorization token in webhooksSecret.
38
+ * That row can only be created through the connect flow against valid MDI
39
+ * Sandbox credentials (add_api_key_integration validates client creds via a live
40
+ * MDI API call), so the end-to-end path is exercised in the Sandbox per the task
41
+ * plan rather than here.
42
+ */
43
+ export const mdi_webhooks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
44
+ log_header("MD Integrations Webhooks Tests")
45
+
46
+ // 1. Missing Authorization header -> 401 (tenant cannot be identified)
47
+ {
48
+ const { status } = await postMDI({ event_type: 'case_approved', case_id: 'mdi-case-test-1' })
49
+ assert(status === 401, `missing Authorization should 401 (got ${status})`, "MDI webhook rejects missing Authorization")
50
+ }
51
+
52
+ // 2. Bogus Authorization token (no matching integration) -> 401
53
+ {
54
+ const { status } = await postMDI(
55
+ { event_type: 'case_approved', case_id: 'mdi-case-test-1' },
56
+ { 'Authorization': `Bearer not-a-real-mdi-token-${Date.now()}` },
57
+ )
58
+ assert(status === 401, `unknown Authorization should 401 (got ${status})`, "MDI webhook rejects unknown Authorization")
59
+ }
60
+
61
+ // 3. Missing event_type -> 400 (bad request), still before any tenant work
62
+ {
63
+ const { status } = await postMDI(
64
+ { case_id: 'mdi-case-test-1' } as any,
65
+ { 'Authorization': `Bearer not-a-real-mdi-token-${Date.now()}` },
66
+ )
67
+ assert(status === 400 || status === 401, `missing event_type should 400/401 (got ${status})`, "MDI webhook rejects payload without event_type")
68
+ }
69
+
70
+ // 4. message_created with bogus auth -> still 401 (auth precedes patient lookup)
71
+ {
72
+ const { status } = await postMDI(
73
+ { event_type: 'message_created', patient_id: 'mdi-patient-x', message_id: 'm-1', user_type: 'App\\Models\\User' },
74
+ { 'Authorization': `Bearer not-a-real-mdi-token-${Date.now()}` },
75
+ )
76
+ assert(status === 401, `message_created with bad auth should 401 (got ${status})`, "MDI webhook authenticates before patient lookup")
77
+ }
78
+ }
79
+
80
+ // Allow running this test file independently
81
+ if (require.main === module) {
82
+ const sdk = new Session({ host })
83
+ const sdkNonAdmin = new Session({ host })
84
+
85
+ const runTests = async () => {
86
+ await setup_tests(sdk, sdkNonAdmin)
87
+ await mdi_webhooks_tests({ sdk, sdkNonAdmin })
88
+ }
89
+
90
+ runTests()
91
+ .then(() => {
92
+ console.log("✅ MD Integrations webhooks test suite completed successfully")
93
+ process.exit(0)
94
+ })
95
+ .catch((error) => {
96
+ console.error("❌ MD Integrations webhooks test suite failed:", error)
97
+ process.exit(1)
98
+ })
99
+ }
@@ -80,6 +80,44 @@ export const no_access_permission_checks_tests = async ({ sdk, sdkNonAdmin } : {
80
80
  const originalRoles = sdkNonAdmin.userInfo.roles
81
81
 
82
82
  try {
83
+ // ========================================
84
+ // Test 0: Healthie proxy_read endpoints are admin-only
85
+ // ========================================
86
+ // These run before any role manipulation, while sdkNonAdmin has its default non-admin role.
87
+ // The admin check is inline with the Healthie dispatch branches (after integration resolution),
88
+ // so a throwaway Healthie integration record must exist for requests to reach it.
89
+ log_header("Test 0: Healthie proxy_read endpoints reject non-admins")
90
+
91
+ const healthieIntegration = await sdk.api.integrations.createOne({
92
+ title: 'Healthie',
93
+ authentication: { type: 'apiKey', info: { access_token: 'dummy-healthie-key-for-admin-only-test', refresh_token: 'unused', scope: '', token_type: 'Bearer', expiry_date: new Date().getTime() } },
94
+ })
95
+ try {
96
+ await async_test(
97
+ "proxy_read healthie patients - should block non-admin user",
98
+ () => sdkNonAdmin.api.integrations.proxy_read({ integration: 'Healthie', type: 'patients' }),
99
+ { shouldError: true, onError: (e: any) => e.message === "Admin access required" }
100
+ )
101
+ await async_test(
102
+ "proxy_read healthie appointment-types - should block non-admin user",
103
+ () => sdkNonAdmin.api.integrations.proxy_read({ integration: 'Healthie', type: 'appointment-types' }),
104
+ { shouldError: true, onError: (e: any) => e.message === "Admin access required" }
105
+ )
106
+ // Admin is not blocked by the role check (the dummy credentials fail upstream in Healthie's API,
107
+ // but never with the 403 role rejection)
108
+ try {
109
+ await sdk.api.integrations.proxy_read({ integration: 'Healthie', type: 'patients', query: JSON.stringify({ pageSize: 1 }) })
110
+ console.log("✅ proxy_read healthie patients - admin allowed")
111
+ } catch (e: any) {
112
+ if (e.message === "Admin access required") {
113
+ throw new Error("proxy_read healthie patients - admin was incorrectly blocked by the role check")
114
+ }
115
+ console.log("✅ proxy_read healthie patients - admin passes role check (dummy Healthie credentials rejected upstream)")
116
+ }
117
+ } finally {
118
+ await sdk.api.integrations.deleteOne(healthieIntegration.id)
119
+ }
120
+
83
121
  // Assign the restricted role to non-admin user
84
122
  await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [noAccessTestRole] }, { replaceObjectFields: true })
85
123
  await wait(undefined, 1500) // wait for role change to propagate
@@ -228,12 +228,134 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
228
228
  }
229
229
  }
230
230
 
231
+ // Block C: Beluga tracking status update events (PACKAGE_*)
232
+ const beluga_tracking_status_block = async ({ sdk }: { sdk: Session }) => {
233
+ log_header("Block C: Beluga tracking status updates (PACKAGE_* events)")
234
+
235
+ const webhookUrl = `${host}/v1/webhooks/beluga`
236
+ const externalOrderId = `EXT-C-${Date.now()}`
237
+ const trackingUrl = 'https://tools.usps.com/go/TrackConfirmAction?tLabels=9400-CCC'
238
+
239
+ const enduser = await sdk.api.endusers.createOne({})
240
+ const form = await sdk.api.forms.createOne({ title: 'Tracking Status Beluga Form' })
241
+ const formResponse = await sdk.api.form_responses.createOne({
242
+ formId: form.id,
243
+ enduserId: enduser.id,
244
+ formTitle: form.title,
245
+ })
246
+
247
+ const inTransitTrigger = await sdk.api.automation_triggers.createOne({
248
+ title: `${TRIGGER_TITLE_BLOCK_B} (In Transit)`,
249
+ status: 'Active',
250
+ event: { type: 'Order Status Equals', info: { source: 'Beluga', status: 'In Transit' } },
251
+ action: buildSetFieldsAction('pharmacy'),
252
+ })
253
+
254
+ const postEvent = async (event: string, info?: object) => {
255
+ const res = await fetch(webhookUrl, {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({
259
+ masterId: `tellescope_${formResponse.id}`,
260
+ event,
261
+ orderId: externalOrderId,
262
+ ...info ? { info } : {},
263
+ }),
264
+ })
265
+ assert(res.status === 200, `Beluga webhook (${event}) expected 200, got ${res.status}`)
266
+ await wait(undefined, 250) // webhook upsert + trigger + Set Fields
267
+ }
268
+
269
+ const getOrder = async () => {
270
+ const orders = await sdk.api.enduser_orders.getSome({
271
+ filter: { source: 'Beluga', externalId: externalOrderId } as any,
272
+ })
273
+ return orders[0]
274
+ }
275
+
276
+ try {
277
+ // Order ships first, then tracking updates arrive
278
+ await postEvent('PHARMACY_ORDER_SHIPPED', { carrier: 'USPS', tracking: '9400-CCC' })
279
+
280
+ await postEvent('PACKAGE_IN_TRANSIT', {
281
+ trackerStatus: 'in_transit',
282
+ trackerId: 'trk_123',
283
+ trackingUrl,
284
+ tracking: '9400-CCC',
285
+ carrier: 'USPS',
286
+ })
287
+
288
+ await async_test(
289
+ "Block C: PACKAGE_IN_TRANSIT fires 'In Transit' trigger with trackingUrl preferred over tracking",
290
+ () => sdk.api.endusers.getOne(enduser.id),
291
+ { onResult: e => !!(
292
+ e.fields?.pharmacy_status === 'In Transit'
293
+ && e.fields?.pharmacy_tracking === trackingUrl
294
+ && e.fields?.pharmacy_carrier === 'USPS'
295
+ && e.fields?.pharmacy_externalId === externalOrderId
296
+ )}
297
+ )
298
+
299
+ await postEvent('PACKAGE_OUT_FOR_DELIVERY', {
300
+ trackerStatus: 'out_for_delivery',
301
+ trackingUrl,
302
+ carrier: 'USPS',
303
+ })
304
+ await async_test(
305
+ "Block C: PACKAGE_OUT_FOR_DELIVERY sets order status 'Out for Delivery'",
306
+ getOrder,
307
+ { onResult: o => o?.status === 'Out for Delivery' }
308
+ )
309
+
310
+ const deliveredDate = new Date().toISOString()
311
+ await postEvent('PACKAGE_DELIVERED', {
312
+ trackerStatus: 'delivered',
313
+ trackingUrl,
314
+ carrier: 'USPS',
315
+ deliveredDate,
316
+ })
317
+ await async_test(
318
+ "Block C: PACKAGE_DELIVERED sets status 'Delivered' and stores deliveredDate",
319
+ getOrder,
320
+ { onResult: o => !!(
321
+ o?.status === 'Delivered'
322
+ && o?.deliveredDate === deliveredDate
323
+ )}
324
+ )
325
+
326
+ await postEvent('PACKAGE_DELIVERY_FAILED', { trackerStatus: 'failure' })
327
+ await async_test(
328
+ "Block C: PACKAGE_DELIVERY_FAILED sets order status 'Delivery Failed'",
329
+ getOrder,
330
+ { onResult: o => o?.status === 'Delivery Failed' }
331
+ )
332
+ } finally {
333
+ // Clean up the order created by the webhook
334
+ try {
335
+ const orders = await sdk.api.enduser_orders.getSome({
336
+ filter: { source: 'Beluga', externalId: externalOrderId } as any,
337
+ })
338
+ for (const o of orders) {
339
+ await sdk.api.enduser_orders.deleteOne(o.id).catch(console.error)
340
+ }
341
+ } catch (err) {
342
+ console.error(err)
343
+ }
344
+
345
+ await sdk.api.automation_triggers.deleteOne(inTransitTrigger.id).catch(console.error)
346
+ await sdk.api.form_responses.deleteOne(formResponse.id).catch(console.error)
347
+ await sdk.api.forms.deleteOne(form.id).catch(console.error)
348
+ await sdk.api.endusers.deleteOne(enduser.id).catch(console.error)
349
+ }
350
+ }
351
+
231
352
  export const set_fields_order_templates_tests = async (
232
353
  { sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }
233
354
  ) => {
234
355
  log_header("Set Fields: {{order.*}} template resolution")
235
356
  await direct_order_creation_block({ sdk })
236
357
  await beluga_webhook_block({ sdk })
358
+ await beluga_tracking_status_block({ sdk })
237
359
  }
238
360
 
239
361
  if (require.main === module) {
@@ -114,6 +114,7 @@ import { organization_settings_duplicates_tests } from "./api_tests/organization
114
114
  import { calendar_events_bulk_update_tests } from "./api_tests/calendar_events_bulk_update.test";
115
115
  import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
116
116
  import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
117
+ import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
117
118
  import { account_switcher_tests } from "./api_tests/account_switcher.test";
118
119
  import { set_fields_order_templates_tests } from "./api_tests/set_fields_order_templates.test";
119
120
  import { date_string_validation_tests } from "./api_tests/date_string_validation.test";
@@ -14357,6 +14358,7 @@ const ip_address_form_tests = async () => {
14357
14358
  await form_submitted_trigger_tests({ sdk, sdkNonAdmin })
14358
14359
  await date_string_validation_tests({ sdk, sdkNonAdmin })
14359
14360
  await openloop_webhooks_tests({ sdk, sdkNonAdmin })
14361
+ await mdi_webhooks_tests({ sdk, sdkNonAdmin })
14360
14362
  await integrations_redacted_tests({ sdk, sdkNonAdmin })
14361
14363
  await mdb_sort_tests({ sdk, sdkNonAdmin })
14362
14364
  await search_tests()
Binary file