@tellescope/sdk 1.255.19 → 1.256.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 (82) hide show
  1. package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  2. package/lib/cjs/tests/api_tests/custom_dashboards.test.js +99 -8
  3. package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  5. package/lib/cjs/tests/api_tests/default_summary_journey.test.js.map +1 -0
  6. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  7. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js +278 -0
  9. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  11. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +212 -33
  12. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  13. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  14. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  15. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  17. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +242 -0
  19. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  21. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +443 -0
  23. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  25. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +231 -0
  27. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  28. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
  29. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
  30. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
  31. package/lib/cjs/tests/tests.d.ts.map +1 -1
  32. package/lib/cjs/tests/tests.js +431 -230
  33. package/lib/cjs/tests/tests.js.map +1 -1
  34. package/lib/esm/enduser.d.ts +0 -1
  35. package/lib/esm/enduser.d.ts.map +1 -1
  36. package/lib/esm/sdk.d.ts +2 -3
  37. package/lib/esm/sdk.d.ts.map +1 -1
  38. package/lib/esm/session.d.ts +0 -1
  39. package/lib/esm/session.d.ts.map +1 -1
  40. package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  41. package/lib/esm/tests/api_tests/custom_dashboards.test.js +99 -8
  42. package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
  43. package/lib/esm/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  44. package/lib/esm/tests/api_tests/default_summary_journey.test.js.map +1 -0
  45. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  46. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  47. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js +248 -0
  48. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  49. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  50. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +213 -34
  51. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  52. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  53. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  54. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  55. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  56. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  57. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +238 -0
  58. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  59. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  60. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  61. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +436 -0
  62. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  63. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  64. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  65. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +224 -0
  66. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  67. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
  68. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
  69. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
  70. package/lib/esm/tests/tests.d.ts.map +1 -1
  71. package/lib/esm/tests/tests.js +431 -230
  72. package/lib/esm/tests/tests.js.map +1 -1
  73. package/lib/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +10 -10
  75. package/src/tests/api_tests/custom_dashboards.test.ts +92 -0
  76. package/src/tests/api_tests/file_download_unicode_names.test.ts +223 -0
  77. package/src/tests/api_tests/openloop_webhooks.test.ts +138 -0
  78. package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +54 -0
  79. package/src/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.ts +215 -0
  80. package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +215 -0
  81. package/src/tests/tests.ts +212 -1
  82. package/test_generated.pdf +0 -0
@@ -17,6 +17,7 @@ import {
17
17
  CompoundFilter,
18
18
  CreateTicketActionInfo,
19
19
  FormResponseValue,
20
+ JourneyContext,
20
21
  ModelName,
21
22
  RoundRobinAssignmentInfo,
22
23
  User,
@@ -41,6 +42,7 @@ import { user_portal_settings_tests } from "./api_tests/user_portal_settings.tes
41
42
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
42
43
  import { healthie_multi_integration_tests } from "./api_tests/healthie_multi_integration.test"
43
44
  import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
45
+ import { file_download_unicode_names_tests } from "./api_tests/file_download_unicode_names.test"
44
46
  import { mdb_sort_tests } from "./api_tests/mdb_sort.test"
45
47
  import { create_user_notifications_trigger_tests } from "./api_tests/create_user_notifications_trigger.test"
46
48
  import { inbox_thread_assignment_updates_tests } from "./api_tests/inbox_thread_assignment_updates.test"
@@ -110,6 +112,7 @@ import { invite_user_enumeration_tests } from "./api_tests/security/F-0007-invit
110
112
  import { register_business_id_injection_tests } from "./api_tests/security/register_business_id_injection.test";
111
113
  import { handle_incoming_communication_cross_tenant_tests } from "./api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test";
112
114
  import { sanitize_user_html_xss_tests } from "./api_tests/security/F-0013-sanitize-user-html.test";
115
+ import { sanitize_user_html_with_iframes_tests } from "./api_tests/security/F-0013-sanitize-user-html-with-iframes.test";
113
116
  import { prototype_pollution_tests } from "./api_tests/security/F-0016-prototype-pollution.test";
114
117
  import { enduser_cross_create_tests } from "./api_tests/security/F-0116-F-0119-enduser-cross-create.test";
115
118
  import { globalunique_leak_tests } from "./api_tests/security/F-0145-globalunique-leak.test";
@@ -7951,9 +7954,49 @@ const merge_enduser_tests = async () => {
7951
7954
  { onResult: e => e.attendees.length === 1 && !!e.attendees?.find(a => a.id === destination.id) }
7952
7955
  )
7953
7956
 
7957
+ // merging two endusers that both lack insurance must leave the fields absent, NOT null.
7958
+ // a stored null breaks every later partial update: those get flattened to dotted paths
7959
+ // ('insurance.memberId'), and MongoDB rejects a dotted $set under a null parent
7960
+ const [noInsuranceSource, noInsuranceDestination] = (await sdk.api.endusers.createSome([
7961
+ { email: 'noins-source@tellescope.com', fname: 'no', lname: 'insurance' },
7962
+ { email: 'noins-destination@tellescope.com' },
7963
+ ])).created
7964
+
7965
+ await sdk.api.endusers.merge({
7966
+ sourceEnduserId: noInsuranceSource.id,
7967
+ destinationEnduserId: noInsuranceDestination.id,
7968
+ })
7969
+
7970
+ await async_test(
7971
+ "Merged endusers have no null insurance",
7972
+ () => sdk.api.endusers.getOne(noInsuranceDestination.id),
7973
+ { onResult: e => e.insurance === undefined && e.insuranceSecondary === undefined }
7974
+ )
7975
+
7976
+ await async_test(
7977
+ "Partial insurance update applies after merge",
7978
+ async () => {
7979
+ await sdk.api.endusers.updateOne(noInsuranceDestination.id, {
7980
+ insurance: { memberId: 'M1', payerName: 'Test Payer' },
7981
+ }) // no replaceObjectFields, matching the standard update from the UI
7982
+ return await sdk.api.endusers.getOne(noInsuranceDestination.id)
7983
+ },
7984
+ { onResult: e => e.insurance?.memberId === 'M1' && e.insurance?.payerName === 'Test Payer' }
7985
+ )
7986
+
7987
+ await async_test(
7988
+ "Second partial insurance update merges",
7989
+ async () => {
7990
+ await sdk.api.endusers.updateOne(noInsuranceDestination.id, { insurance: { groupNumber: 'G1' } })
7991
+ return await sdk.api.endusers.getOne(noInsuranceDestination.id)
7992
+ },
7993
+ { onResult: e => e.insurance?.groupNumber === 'G1' && e.insurance?.memberId === 'M1' }
7994
+ )
7995
+
7954
7996
  await Promise.all([
7955
7997
  sdk.api.endusers.deleteOne(destination.id),
7956
7998
  sdk.api.endusers.deleteOne(otherEnduser.id),
7999
+ sdk.api.endusers.deleteOne(noInsuranceDestination.id),
7957
8000
  sdk.api.calendar_events.deleteOne(event.id),
7958
8001
  sdk.api.calendar_events.deleteOne(eventToMove.id),
7959
8002
  ])
@@ -8587,6 +8630,8 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8587
8630
  enduser,
8588
8631
  closedForReason,
8589
8632
  testDelayedChild,
8633
+ calendarEventId,
8634
+ ticketOwner,
8590
8635
  } : {
8591
8636
  assignedTo: string[]
8592
8637
  info: Pick<CreateTicketActionInfo, 'assignmentStrategy' | 'defaultAssignee'>,
@@ -8594,6 +8639,8 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8594
8639
  closedForReason?: string,
8595
8640
  enduser?: Enduser,
8596
8641
  testDelayedChild?: boolean,
8642
+ calendarEventId?: string, // copied into journeyContext by close_ticket
8643
+ ticketOwner?: string, // override the seed ticket's owner, to avoid masking a previous-owner fallback
8597
8644
  }) => {
8598
8645
 
8599
8646
  const e = enduser || await sdk.api.endusers.createOne({ assignedTo, journeys: { [journey.id]: '' }, fields: { Tag: "tag3"} })
@@ -8631,8 +8678,9 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8631
8678
  enduserId: e.id,
8632
8679
  automationStepId: PLACEHOLDER_ID,
8633
8680
  journeyId: journey.id,
8634
- owner: validOwners[0],
8681
+ owner: ticketOwner ?? validOwners[0],
8635
8682
  closedForReason,
8683
+ ...(calendarEventId ? { calendarEventId } : {}),
8636
8684
  })
8637
8685
 
8638
8686
  await async_test(
@@ -8765,11 +8813,13 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8765
8813
  info,
8766
8814
  validOwners,
8767
8815
  enduser,
8816
+ journeyContext,
8768
8817
  } : {
8769
8818
  assignedTo: string[]
8770
8819
  info: Pick<CreateTicketActionInfo, 'assignmentStrategy' | 'defaultAssignee'>,
8771
8820
  validOwners: string[],
8772
8821
  enduser?: Enduser,
8822
+ journeyContext?: JourneyContext,
8773
8823
  }) => {
8774
8824
  const e = enduser || await sdk.api.endusers.createOne({ assignedTo, fields: { Tag: 'tag3' } })
8775
8825
  await sdk.api.automated_actions.createOne({
@@ -8780,6 +8830,7 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8780
8830
  journeyId: journey.id,
8781
8831
  status: 'active',
8782
8832
  processAfter: Date.now(),
8833
+ ...(journeyContext ? { journeyContext } : {}),
8783
8834
  })
8784
8835
 
8785
8836
  await async_test(
@@ -8880,6 +8931,115 @@ const test_ticket_automation_assignment_and_optimization = async () => {
8880
8931
  validOwners: [sdkNonAdmin.userInfo.id],
8881
8932
  })
8882
8933
 
8934
+ // 'Recently-Booked Appointment Host' assignment
8935
+ // journeyContext.calendarEventId, when it resolves to a host, takes priority over the legacy
8936
+ // "newest event for this enduser" lookup. one enduser per case, to avoid trigger throttling.
8937
+ const HOST_A = sdkNonAdmin.userInfo.id
8938
+ const HOST_B = sdk.userInfo.id
8939
+ const appointmentHostInfo = (defaultAssignee: string) => ({
8940
+ assignmentStrategy: { type: 'Recently-Booked Appointment Host', info: {} } as const,
8941
+ defaultAssignee,
8942
+ })
8943
+ // the legacy filter matches an event having *some* user attendee and *some* attendee with this
8944
+ // enduser's id, so each hosted event carries both
8945
+ const createHostedEvent = (enduserId: string, hostId: string) => sdk.api.calendar_events.createOne({
8946
+ title: 'appointment host test',
8947
+ durationInMinutes: 30,
8948
+ startTimeInMS: Date.now(),
8949
+ attendees: [{ type: 'enduser', id: enduserId }, { type: 'user', id: hostId }],
8950
+ })
8951
+
8952
+ // CORE PROOF: two appointments with different hosts, journey context names the OLDER one.
8953
+ // fails before this fix, which always took the newest-created event (HOST_B).
8954
+ {
8955
+ const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
8956
+ const older = await createHostedEvent(e.id, HOST_A)
8957
+ const newer = await createHostedEvent(e.id, HOST_B) // higher _id, so legacy sort picks this one
8958
+ await testBackgroundTicket({
8959
+ assignedTo: [],
8960
+ enduser: e,
8961
+ info: appointmentHostInfo(HOST_B), // distinct from expected owner, so a fallback would fail
8962
+ validOwners: [HOST_A],
8963
+ journeyContext: { calendarEventId: older.id },
8964
+ })
8965
+ await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
8966
+ }
8967
+
8968
+ // no journey context -> legacy "newest event" behavior is unchanged
8969
+ {
8970
+ const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
8971
+ const older = await createHostedEvent(e.id, HOST_A)
8972
+ const newer = await createHostedEvent(e.id, HOST_B)
8973
+ await testBackgroundTicket({
8974
+ assignedTo: [],
8975
+ enduser: e,
8976
+ info: appointmentHostInfo(HOST_A),
8977
+ validOwners: [HOST_B], // newest created event's host
8978
+ })
8979
+ await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
8980
+ }
8981
+
8982
+ // context event no longer exists -> falls back to the legacy lookup
8983
+ {
8984
+ const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
8985
+ const deleted = await createHostedEvent(e.id, HOST_B)
8986
+ await sdk.api.calendar_events.deleteOne(deleted.id)
8987
+ const hosted = await createHostedEvent(e.id, HOST_A)
8988
+ await testBackgroundTicket({
8989
+ assignedTo: [],
8990
+ enduser: e,
8991
+ info: appointmentHostInfo(HOST_B),
8992
+ validOwners: [HOST_A],
8993
+ journeyContext: { calendarEventId: deleted.id },
8994
+ })
8995
+ await sdk.api.calendar_events.deleteOne(hosted.id)
8996
+ }
8997
+
8998
+ // context event has no user attendee (so yields no host) -> falls back to the legacy lookup
8999
+ {
9000
+ const e = await sdk.api.endusers.createOne({ fields: { Tag: 'tag3' } })
9001
+ const enduserOnly = await sdk.api.calendar_events.createOne({
9002
+ title: 'appointment host test, no host',
9003
+ durationInMinutes: 30,
9004
+ startTimeInMS: Date.now(),
9005
+ attendees: [{ type: 'enduser', id: e.id }],
9006
+ })
9007
+ const hosted = await createHostedEvent(e.id, HOST_A)
9008
+ await testBackgroundTicket({
9009
+ assignedTo: [],
9010
+ enduser: e,
9011
+ info: appointmentHostInfo(HOST_B),
9012
+ validOwners: [HOST_A],
9013
+ journeyContext: { calendarEventId: enduserOnly.id },
9014
+ })
9015
+ await Promise.all([enduserOnly, hosted].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
9016
+ }
9017
+
9018
+ // no events at all -> no resolvable host, so defaultAssignee wins
9019
+ await testBackgroundTicket({
9020
+ assignedTo: [],
9021
+ info: appointmentHostInfo(HOST_A),
9022
+ validOwners: [HOST_A],
9023
+ })
9024
+
9025
+ // api path: close_ticket copies the original ticket's calendarEventId into journeyContext
9026
+ {
9027
+ const e = await sdk.api.endusers.createOne({
9028
+ journeys: { [journey.id]: '' }, fields: { Tag: 'tag3' },
9029
+ })
9030
+ const older = await createHostedEvent(e.id, HOST_A)
9031
+ const newer = await createHostedEvent(e.id, HOST_B)
9032
+ await testForegroundTicket({
9033
+ assignedTo: [],
9034
+ enduser: e,
9035
+ info: appointmentHostInfo(HOST_B),
9036
+ validOwners: [HOST_A],
9037
+ calendarEventId: older.id,
9038
+ ticketOwner: HOST_B, // so a previous-owner style fallback would also fail
9039
+ })
9040
+ await Promise.all([older, newer].map(ev => sdk.api.calendar_events.deleteOne(ev.id)))
9041
+ }
9042
+
8883
9043
  return Promise.all([
8884
9044
  await sdk.api.journeys.deleteOne(journey.id),
8885
9045
  await sdk.api.ticket_queues.deleteOne(queue.id),
@@ -13764,6 +13924,55 @@ const replace_form_field_template_values_tests = async () => {
13764
13924
  ) === '<p>Locations: NYC<br />SF<br />LA</p>',
13765
13925
  'fail CRLF in substituted value', 'CRLF in substituted value'
13766
13926
  )
13927
+
13928
+ // escapeHTMLValues: substituted values are patient-supplied (their own intake answers / enduser
13929
+ // record), so on any path rendered by a sanitizer that permits iframes they must be escaped —
13930
+ // otherwise a patient can inject a frame into an admin-authored description.
13931
+ const enduserWithMarkupField = {
13932
+ fname: "Multi",
13933
+ fields: { Injected: '<iframe src="https://evil.example"></iframe>' },
13934
+ } as Partial<Enduser>
13935
+
13936
+ const escaped = replace_form_field_template_values(
13937
+ '<p>{{enduser.Injected}}</p>',
13938
+ { enduser: enduserWithMarkupField, escapeNewlinesAsHTMLBreaks: true, escapeHTMLValues: true }
13939
+ )
13940
+ assert(
13941
+ escaped.includes('&lt;iframe') && !escaped.includes('<iframe'),
13942
+ 'fail escapeHTMLValues escapes markup', 'escapeHTMLValues escapes markup'
13943
+ )
13944
+
13945
+ // Escaping must run BEFORE the newline conversion, or the injected breaks become &lt;br /&gt;
13946
+ const enduserWithMarkupAndNewline = {
13947
+ fields: { Mixed: 'a<b\nc&d' },
13948
+ } as Partial<Enduser>
13949
+ assert(
13950
+ replace_form_field_template_values(
13951
+ '{{enduser.Mixed}}',
13952
+ { enduser: enduserWithMarkupAndNewline, escapeNewlinesAsHTMLBreaks: true, escapeHTMLValues: true }
13953
+ ) === 'a&lt;b<br />c&amp;d',
13954
+ 'fail escapeHTMLValues ordering vs newline breaks', 'escapeHTMLValues ordering vs newline breaks'
13955
+ )
13956
+
13957
+ // Flag absent => byte-identical to today. Plain-text callers (field titles/descriptions rendered
13958
+ // in a Typography) rely on this: escaping there would corrupt e.g. a name containing '&'.
13959
+ const enduserWithAmpersand = {
13960
+ fields: { Practice: 'Smith & Jones <Cardiology>' },
13961
+ } as Partial<Enduser>
13962
+ assert(
13963
+ replace_form_field_template_values(
13964
+ 'Practice: {{enduser.Practice}}',
13965
+ { enduser: enduserWithAmpersand }
13966
+ ) === 'Practice: Smith & Jones <Cardiology>',
13967
+ 'fail no escaping without the flag', 'no escaping without the flag'
13968
+ )
13969
+ assert(
13970
+ replace_form_field_template_values(
13971
+ 'Practice: {{enduser.Practice}}',
13972
+ { enduser: enduserWithAmpersand, escapeHTMLValues: true }
13973
+ ) === 'Practice: Smith &amp; Jones &lt;Cardiology&gt;',
13974
+ 'fail escaping with the flag', 'escaping with the flag'
13975
+ )
13767
13976
  }
13768
13977
 
13769
13978
  const inbox_threads_building_tests = async () => {
@@ -15095,6 +15304,7 @@ const ip_address_form_tests = async () => {
15095
15304
  await cascade_role_rename_cross_tenant_tests({ sdk, sdkNonAdmin })
15096
15305
  await self_admin_role_assignment_tests({ sdk, sdkNonAdmin })
15097
15306
  await sanitize_user_html_xss_tests()
15307
+ await sanitize_user_html_with_iframes_tests()
15098
15308
  await prototype_pollution_tests()
15099
15309
  await account_switcher_tests({ sdk, sdkNonAdmin })
15100
15310
  await totp_mfa_tests({ sdk, sdkNonAdmin })
@@ -15129,6 +15339,7 @@ const ip_address_form_tests = async () => {
15129
15339
  await time_tracks_appointment_duration_tests({ sdk, sdkNonAdmin })
15130
15340
  await calendar_event_limits_tests({ sdk, sdkNonAdmin })
15131
15341
  await get_some_projection_tests({ sdk, sdkNonAdmin })
15342
+ await file_download_unicode_names_tests({ sdk, sdkNonAdmin })
15132
15343
  await elation_user_id_tests({ sdk, sdkNonAdmin })
15133
15344
  await concurrent_build_threads_tests({ sdk, sdkNonAdmin })
15134
15345
  await custom_aggregation_tests({ sdk, sdkNonAdmin })
Binary file