@tellescope/sdk 1.256.16 → 2.0.0-beta.1
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/CLAUDE.md +5 -0
- package/env.example +39 -0
- package/lib/cjs/enduser.d.ts +1 -0
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/sdk.d.ts +6 -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/session.d.ts +1 -0
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/active_patient_count.test.js +1 -1
- package/lib/cjs/tests/api_tests/active_patient_count.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/appointment_rescheduled_trigger.test.js +1 -1
- package/lib/cjs/tests/api_tests/appointment_rescheduled_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js +5 -5
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +21 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +215 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +6 -6
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +3 -3
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +257 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/managed_content_enduser_access.test.js +1 -1
- package/lib/cjs/tests/api_tests/managed_content_enduser_access.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js +1 -1
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +139 -9
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.js +21 -8
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/nutrition_tracking.test.js +1 -1
- package/lib/cjs/tests/api_tests/nutrition_tracking.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/programs.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/programs.test.js +370 -0
- package/lib/cjs/tests/api_tests/programs.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js +295 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +55 -47
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +484 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js +2 -2
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +297 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js +202 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js +233 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.js +235 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.js +1 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +395 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +471 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js +1 -1
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js +233 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +569 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
- package/lib/cjs/tests/public_endpoint_tests.js +3 -2
- package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
- package/lib/cjs/tests/setup.d.ts +16 -0
- package/lib/cjs/tests/setup.d.ts.map +1 -1
- package/lib/cjs/tests/setup.js +55 -8
- package/lib/cjs/tests/setup.js.map +1 -1
- package/lib/cjs/tests/setup_isolated.d.ts +9 -0
- package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
- package/lib/cjs/tests/setup_isolated.js +23 -2
- package/lib/cjs/tests/setup_isolated.js.map +1 -1
- package/lib/cjs/tests/socket_tests.d.ts.map +1 -1
- package/lib/cjs/tests/socket_tests.js +3 -2
- package/lib/cjs/tests/socket_tests.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +244 -216
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js +118 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +3 -2
- package/lib/cjs/tests/webhooks_tests.js.map +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/esm/enduser.d.ts +2 -0
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +9 -3
- 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/session.d.ts +2 -0
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/active_patient_count.test.js +1 -1
- package/lib/esm/tests/api_tests/active_patient_count.test.js.map +1 -1
- package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js +1 -1
- package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js +5 -5
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js +1 -1
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +21 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +210 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +7 -7
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +4 -4
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js +252 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -0
- package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js +1 -1
- package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js.map +1 -1
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js +1 -1
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +139 -9
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/mdi_webhooks.test.js +21 -8
- package/lib/esm/tests/api_tests/mdi_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/nutrition_tracking.test.js +1 -1
- package/lib/esm/tests/api_tests/nutrition_tracking.test.js.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/programs.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/programs.test.js +365 -0
- package/lib/esm/tests/api_tests/programs.test.js.map +1 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.js +290 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +55 -47
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +479 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js +3 -3
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +292 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.js +198 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js +229 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.js +231 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.js.map +1 -0
- package/lib/esm/tests/api_tests/time_tracks.test.js +1 -1
- package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +390 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +466 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.js +1 -1
- package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.js +229 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +564 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
- package/lib/esm/tests/public_endpoint_tests.js +3 -2
- package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
- package/lib/esm/tests/setup.d.ts +16 -0
- package/lib/esm/tests/setup.d.ts.map +1 -1
- package/lib/esm/tests/setup.js +53 -7
- package/lib/esm/tests/setup.js.map +1 -1
- package/lib/esm/tests/setup_isolated.d.ts +9 -0
- package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
- package/lib/esm/tests/setup_isolated.js +21 -1
- package/lib/esm/tests/setup_isolated.js.map +1 -1
- package/lib/esm/tests/socket_tests.d.ts.map +1 -1
- package/lib/esm/tests/socket_tests.js +3 -2
- package/lib/esm/tests/socket_tests.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +244 -216
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.js +115 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +3 -2
- package/lib/esm/tests/webhooks_tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -19
- package/src/sdk.ts +11 -1
- package/src/tests/api_tests/active_patient_count.test.ts +1 -1
- package/src/tests/api_tests/appointment_rescheduled_trigger.test.ts +1 -1
- package/src/tests/api_tests/calendar_event_references_update.test.ts +1 -1
- package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +1 -1
- package/src/tests/api_tests/enduser_fields_sync.test.ts +150 -0
- package/src/tests/api_tests/enduser_login_rate_limits.test.ts +7 -7
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +4 -4
- package/src/tests/api_tests/load_threads_care_team.test.ts +210 -0
- package/src/tests/api_tests/managed_content_enduser_access.test.ts +1 -1
- package/src/tests/api_tests/managed_content_file_access.test.ts +1 -1
- package/src/tests/api_tests/mdi_case_offerings.test.ts +138 -0
- package/src/tests/api_tests/mdi_webhooks.test.ts +9 -0
- package/src/tests/api_tests/nutrition_tracking.test.ts +1 -1
- package/src/tests/api_tests/openloop_webhooks.test.ts +1 -1
- package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +33 -26
- package/src/tests/api_tests/security/allowed-paths-confinement.test.ts +1 -1
- package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +432 -0
- package/src/tests/api_tests/security/booking-info-idor-token-mint.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-cross-create.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-write-restrictions.test.ts +1 -1
- package/src/tests/api_tests/security/invite-user-enumeration.test.ts +3 -3
- package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +197 -0
- package/src/tests/api_tests/security/sanitize-user-html-with-iframes.test.ts +4 -1
- package/src/tests/api_tests/templated_message_date_fields.test.ts +146 -0
- package/src/tests/api_tests/templated_message_links.test.ts +192 -0
- package/src/tests/api_tests/time_tracks.test.ts +1 -1
- package/src/tests/api_tests/trigger_contains_matching.test.ts +345 -0
- package/src/tests/api_tests/user_logs_restore_deleted.test.ts +415 -0
- package/src/tests/api_tests/user_portal_settings.test.ts +1 -1
- package/src/tests/api_tests/validation_behavior_locks.test.ts +204 -0
- package/src/tests/public_endpoint_tests.ts +3 -2
- package/src/tests/setup.ts +43 -7
- package/src/tests/setup_isolated.ts +10 -0
- package/src/tests/socket_tests.ts +3 -2
- package/src/tests/tests.ts +22 -2
- package/src/tests/unit_tests/trigger_title_partials.test.ts +174 -0
- package/src/tests/webhooks_tests.ts +4 -2
- package/test_generated.pdf +0 -0
- package/tsconfig.cjs.json +17 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { EnduserSession } from "../../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
assert_checks,
|
|
6
|
+
Check,
|
|
7
|
+
setup_isolated_tenant,
|
|
8
|
+
} from "../../setup_isolated"
|
|
9
|
+
|
|
10
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
+
|
|
12
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Form "Rich Text" answers must be sanitized when they are stored, not only when they are rendered.
|
|
16
|
+
*
|
|
17
|
+
* A Rich Text answer is HTML authored by whoever fills the form — including an unauthenticated
|
|
18
|
+
* visitor on a public intake page — and it is stored verbatim by the generic write path (the only
|
|
19
|
+
* thing it strips is `<script>`). Every read path re-sanitizes before `dangerouslySetInnerHTML`,
|
|
20
|
+
* but the staff-side rich text *editor* did not: `@tellescope/react-components`'s `RichTextInput`
|
|
21
|
+
* fed the stored answer straight into `html-to-draftjs`, whose `<iframe>` handling produces a
|
|
22
|
+
* Draft.js EMBEDDED_LINK block that `react-draft-wysiwyg` renders as a real, unsandboxed iframe.
|
|
23
|
+
* So an enduser could submit `<iframe src="javascript:…">` in an answer and have it execute in the
|
|
24
|
+
* session of the staff member who opened that response for editing (enduser -> staff stored XSS,
|
|
25
|
+
* i.e. a PHI-egress class). The editor was fixed to sanitize before parsing; this test covers the
|
|
26
|
+
* second layer: the answer is sanitized once, on write, so exports, PDFs, integrations, AI
|
|
27
|
+
* summaries and any future consumer inherit it instead of each having to remember.
|
|
28
|
+
*
|
|
29
|
+
* Every client write of answers goes through `formResponseValidator` — generic create/update
|
|
30
|
+
* (endusers may PATCH `responses`), `submit_form_response` and `save_field_response` — so all three
|
|
31
|
+
* are exercised here. The server-side writers that bypass validation (Formsort, Healthie sync, AI
|
|
32
|
+
* form filling) never construct a `'Rich Text'` answer; see the note on the validator in
|
|
33
|
+
* `@tellescope/validation`.
|
|
34
|
+
*
|
|
35
|
+
* The negative checks are paired with positive ones: formatting and legitimate external links must
|
|
36
|
+
* survive, or a "sanitized" result could just mean the answer was discarded. The last two checks
|
|
37
|
+
* guard the two ways this change could break normal use: an empty answer must round-trip
|
|
38
|
+
* unchanged (the portal compares against `''` / `'<p></p>'`), and a long answer must not be
|
|
39
|
+
* rejected because the sanitizer's own `target`/`rel` additions pushed it over the length limit.
|
|
40
|
+
*/
|
|
41
|
+
export const rich_text_answer_sanitized_on_write_checks = async (): Promise<Check[]> => {
|
|
42
|
+
const checks: Check[] = []
|
|
43
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
44
|
+
|
|
45
|
+
const PAYLOAD = '<p>hi <b>bold</b> <a href="https://x.io">l</a></p>'
|
|
46
|
+
+ '<iframe src="javascript:alert(1)"></iframe>'
|
|
47
|
+
+ '<img src="x" onerror="alert(1)">'
|
|
48
|
+
+ '<script>alert(1)</script>'
|
|
49
|
+
+ '<a href="javascript:alert(2)">j</a>'
|
|
50
|
+
|
|
51
|
+
const dangerous = (stored: string) => ([
|
|
52
|
+
['<iframe', stored.toLowerCase().includes('<iframe')],
|
|
53
|
+
['onerror', stored.toLowerCase().includes('onerror')],
|
|
54
|
+
['<script', stored.toLowerCase().includes('<script')],
|
|
55
|
+
['javascript:', stored.toLowerCase().includes('javascript:')],
|
|
56
|
+
] as [string, boolean][]).filter(([, present]) => present).map(([name]) => name)
|
|
57
|
+
|
|
58
|
+
const record_sanitized = (label: string, stored: string | undefined) => {
|
|
59
|
+
if (typeof stored !== 'string') {
|
|
60
|
+
record(label, false, `no answer stored (got ${JSON.stringify(stored)})`)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const bad = dangerous(stored)
|
|
64
|
+
const keptFormatting = stored.includes('<b>bold</b>')
|
|
65
|
+
const keptLink = stored.includes('href="https://x.io"') && stored.includes('rel="noopener noreferrer"')
|
|
66
|
+
|
|
67
|
+
record(label, bad.length === 0 && keptFormatting && keptLink,
|
|
68
|
+
bad.length !== 0 ? `dangerous markup stored: ${bad.join(', ')} — ${stored.slice(0, 300)}`
|
|
69
|
+
: !keptFormatting ? `formatting did not survive — ${stored.slice(0, 300)}`
|
|
70
|
+
: !keptLink ? `external link lost its href or rel — ${stored.slice(0, 300)}`
|
|
71
|
+
: `stored sanitized: ${stored.slice(0, 300)}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
tenant = await setup_isolated_tenant({ name: `RichText ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0 })
|
|
79
|
+
const sdk = tenant.sdk
|
|
80
|
+
|
|
81
|
+
const form = await sdk.api.forms.createOne({ title: `rich text ${RAND()}` })
|
|
82
|
+
const field = await sdk.api.form_fields.createOne({
|
|
83
|
+
formId: form.id, title: 'Tell us more', type: 'Rich Text', previousFields: [{ type: 'root', info: {} }],
|
|
84
|
+
})
|
|
85
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
86
|
+
email: `richtext-${RAND()}@tellescope.com`, fname: 'Rich', lname: 'Text',
|
|
87
|
+
})
|
|
88
|
+
// generate_auth_token rather than .authenticate — repeated logins hit the rate limiter.
|
|
89
|
+
const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
|
|
90
|
+
const enduserSDK = new EnduserSession({ host, businessId: tenant.businessId, authToken })
|
|
91
|
+
|
|
92
|
+
const prepare = () => sdk.api.form_responses.prepare_form_response({ formId: form.id, enduserId: enduser.id })
|
|
93
|
+
const answer = (value: string) => ({
|
|
94
|
+
fieldId: field.id, fieldTitle: field.title, answer: { type: 'Rich Text' as const, value },
|
|
95
|
+
})
|
|
96
|
+
/** What is actually in the database, read back by an admin session. */
|
|
97
|
+
const stored_answer = async (formResponseId: string) => {
|
|
98
|
+
const formResponse = await sdk.api.form_responses.getOne(formResponseId)
|
|
99
|
+
const response = (formResponse.responses || []).find(r => r.fieldId === field.id)
|
|
100
|
+
return (response?.answer as { value?: string } | undefined)?.value
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ===== 1. submit_form_response — the path a patient filling out a form takes ===== */
|
|
104
|
+
|
|
105
|
+
const submitted = await prepare()
|
|
106
|
+
await enduserSDK.api.form_responses.submit_form_response({
|
|
107
|
+
accessCode: submitted.accessCode, responses: [answer(PAYLOAD)],
|
|
108
|
+
})
|
|
109
|
+
const submittedValue = await stored_answer(submitted.response.id)
|
|
110
|
+
record_sanitized('1: submit_form_response stores a sanitized answer', submittedValue)
|
|
111
|
+
|
|
112
|
+
/* ===== 2. save_field_response — the partial/draft save the portal fires per question ===== */
|
|
113
|
+
|
|
114
|
+
const partial = await prepare()
|
|
115
|
+
await enduserSDK.api.form_responses.save_field_response({
|
|
116
|
+
accessCode: partial.accessCode, response: answer(PAYLOAD),
|
|
117
|
+
})
|
|
118
|
+
record_sanitized('2: save_field_response stores a sanitized answer', await stored_answer(partial.response.id))
|
|
119
|
+
|
|
120
|
+
/* ===== 3. generic PATCH of responses — endusers are allowed to update their own ===== */
|
|
121
|
+
|
|
122
|
+
const patched = await prepare()
|
|
123
|
+
await enduserSDK.api.form_responses.updateOne(patched.response.id, { responses: [answer(PAYLOAD)] })
|
|
124
|
+
record_sanitized('3: enduser PATCH of responses stores a sanitized answer', await stored_answer(patched.response.id))
|
|
125
|
+
|
|
126
|
+
/* ===== 4. Idempotence: re-submitting what was stored must not change it again ===== */
|
|
127
|
+
|
|
128
|
+
if (typeof submittedValue === 'string') {
|
|
129
|
+
const resubmitted = await prepare()
|
|
130
|
+
await enduserSDK.api.form_responses.submit_form_response({
|
|
131
|
+
accessCode: resubmitted.accessCode, responses: [answer(submittedValue)],
|
|
132
|
+
})
|
|
133
|
+
const again = await stored_answer(resubmitted.response.id)
|
|
134
|
+
record('4: re-submitting a sanitized answer is idempotent', again === submittedValue,
|
|
135
|
+
again === submittedValue ? 'unchanged on the second pass' : `changed — ${String(again).slice(0, 300)}`)
|
|
136
|
+
} else {
|
|
137
|
+
record('4: re-submitting a sanitized answer is idempotent', false, 'skipped: check 1 stored nothing')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ===== 5. Empty answers round-trip: the portal treats '' and '<p></p>' as "unanswered" ===== */
|
|
141
|
+
|
|
142
|
+
const empties = await prepare()
|
|
143
|
+
const roundTrips: string[] = []
|
|
144
|
+
for (const value of ['<p></p>', '']) {
|
|
145
|
+
await enduserSDK.api.form_responses.save_field_response({
|
|
146
|
+
accessCode: empties.accessCode, response: answer(value),
|
|
147
|
+
})
|
|
148
|
+
roundTrips.push(`${JSON.stringify(value)} -> ${JSON.stringify(await stored_answer(empties.response.id))}`)
|
|
149
|
+
}
|
|
150
|
+
const emptiesOk = roundTrips[0] === '"<p></p>" -> "<p></p>"' && roundTrips[1] === '"" -> ""'
|
|
151
|
+
record('5: empty and <p></p> answers round-trip unchanged', emptiesOk, roundTrips.join(', '))
|
|
152
|
+
|
|
153
|
+
/* ===== 6. Length headroom: the sanitizer ADDS target/rel to every external link ===== */
|
|
154
|
+
|
|
155
|
+
// ~24.9k of legitimate answer, just under the old 25000 limit. Sanitizing adds ~41 characters
|
|
156
|
+
// per external link, so at the old limit the sanitized result would be rejected on a re-save.
|
|
157
|
+
const LINKS = 12
|
|
158
|
+
const longAnswer = `<p>${'a'.repeat(24400)}</p>`
|
|
159
|
+
+ `<a href="https://example.com/a">link</a>`.repeat(LINKS)
|
|
160
|
+
const longResponse = await prepare()
|
|
161
|
+
const longResult = await enduserSDK.api.form_responses.submit_form_response({
|
|
162
|
+
accessCode: longResponse.accessCode, responses: [answer(longAnswer)],
|
|
163
|
+
}).then(() => stored_answer(longResponse.response.id))
|
|
164
|
+
.catch((e: any) => `rejected: ${e?.message || e}`)
|
|
165
|
+
|
|
166
|
+
const longOk = typeof longResult === 'string'
|
|
167
|
+
&& longResult.split('rel="noopener noreferrer"').length - 1 === LINKS
|
|
168
|
+
record('6: a ~24.9k answer with external links is accepted after sanitizing', longOk,
|
|
169
|
+
longOk ? `stored ${longResult.length} characters with ${LINKS} rel-tagged links (submitted ${longAnswer.length})`
|
|
170
|
+
: `submitted ${longAnswer.length} characters — ${String(longResult).slice(0, 300)}`)
|
|
171
|
+
} finally {
|
|
172
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
|
|
173
|
+
// would strand the provisioned tenant permanently.
|
|
174
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return checks
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
181
|
+
export const rich_text_answer_sanitized_on_write_tests = async () => {
|
|
182
|
+
assert_checks('Rich Text answers sanitized on write', await rich_text_answer_sanitized_on_write_checks())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow running this test file independently
|
|
186
|
+
if (require.main === module) {
|
|
187
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
188
|
+
rich_text_answer_sanitized_on_write_tests()
|
|
189
|
+
.then(() => {
|
|
190
|
+
console.log("✅ rich text answer sanitization test suite completed successfully")
|
|
191
|
+
process.exit(0)
|
|
192
|
+
})
|
|
193
|
+
.catch((error) => {
|
|
194
|
+
console.error("❌ rich text answer sanitization test suite failed:", error)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
@@ -147,7 +147,10 @@ export const sanitize_user_html_with_iframes_tests = async () => {
|
|
|
147
147
|
// sandbox="" is the MOST restrictive value and must not be replaced by the default
|
|
148
148
|
const emptySandbox = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" sandbox=""></iframe>`)
|
|
149
149
|
if (emptySandbox.includes('allow-')) fail(`empty sandbox was replaced with the permissive default: ${emptySandbox}`)
|
|
150
|
-
|
|
150
|
+
// Accepts either spelling — a bare `sandbox` and `sandbox=""` are identical per the HTML spec,
|
|
151
|
+
// and which one sanitize-html emits has changed between releases. What must never happen is the
|
|
152
|
+
// attribute disappearing, which leaves the frame with no sandbox at all.
|
|
153
|
+
if (!/\ssandbox(=""|\s|>)/.test(emptySandbox)) fail(`empty sandbox attribute lost entirely: ${emptySandbox}`)
|
|
151
154
|
|
|
152
155
|
// author referrerpolicy honored
|
|
153
156
|
const noref = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" referrerpolicy="no-referrer"></iframe>`)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assert_checks,
|
|
5
|
+
Check,
|
|
6
|
+
setup_isolated_tenant,
|
|
7
|
+
} from "../setup_isolated"
|
|
8
|
+
|
|
9
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Coverage for how `get_templated_message` renders enduser field values that LOOK like dates.
|
|
13
|
+
*
|
|
14
|
+
* The rule: a value stored as text renders as the author stored it. `message_templating.ts` only
|
|
15
|
+
* reformats an actual `Date` object, and a custom field is always a string.
|
|
16
|
+
*
|
|
17
|
+
* This exists because that rule was silently broken IN PRODUCTION, and had been for a long time.
|
|
18
|
+
* The templating code read `if (fieldValue instanceof Date || isDate(fieldValue))`. Validator
|
|
19
|
+
* 13.11.0's `isDate` was timezone-dependent — it compared a UTC-parsed date against a local-time
|
|
20
|
+
* `.getDate()` — so it returned false on a US developer's machine but TRUE in UTC, which is what
|
|
21
|
+
* the servers run. A date of birth stored as "1985-03-12" therefore took the Date branch in
|
|
22
|
+
* production, was parsed as UTC midnight and localized to the enduser's timezone, and rendered in a
|
|
23
|
+
* patient-facing SMS or email as "3/11/1985, 7:00:00 PM" — the PREVIOUS day, with a time nobody
|
|
24
|
+
* entered. A January date lost a year: "2024-01-01" rendered "12/31/2023".
|
|
25
|
+
*
|
|
26
|
+
* The validator bump did not cause this; it made it reproducible locally, which is how it surfaced.
|
|
27
|
+
* Removing the `isDate` half of the condition fixes it in every timezone.
|
|
28
|
+
*
|
|
29
|
+
* This is the ONE intentional production behavior change in an otherwise behavior-neutral
|
|
30
|
+
* dependency PR, made deliberately rather than shipping wrong dates to patients for another
|
|
31
|
+
* cycle. Every other validator touched here was pinned to production's existing behavior.
|
|
32
|
+
*
|
|
33
|
+
* So the assertions below are about the day not moving and no clock time appearing, which is what a
|
|
34
|
+
* patient would actually notice. A pure-function test could not have caught this: the function that
|
|
35
|
+
* does the work is module-private and the endpoint is the real surface.
|
|
36
|
+
*
|
|
37
|
+
* Runs standalone or as part of the main suite:
|
|
38
|
+
* npm run test:modular -- api_tests/templated_message_date_fields
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** Values a customer realistically stores in a date-ish custom field, and what must come back. */
|
|
42
|
+
const DATE_SHAPED_VALUES = [
|
|
43
|
+
{ label: 'ISO date (the shape that regressed)', value: '1985-03-12' },
|
|
44
|
+
{ label: 'ISO date, recent', value: '2024-01-01' },
|
|
45
|
+
{ label: 'slash-separated date', value: '1999/12/31' },
|
|
46
|
+
{ label: 'MM-DD-YYYY (the dateOfBirth format)', value: '03-12-1985' },
|
|
47
|
+
{ label: 'ISO datetime string', value: '2024-06-15T14:30:00.000Z' },
|
|
48
|
+
{ label: 'leap day', value: '2000-02-29' },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/** A rendered date-only value must not have grown a clock time or a timezone abbreviation. */
|
|
52
|
+
const LOOKS_REFORMATTED = /\d{1,2}:\d{2}|\bAM\b|\bPM\b|\bE[SD]T\b|\bUTC\b|\bGMT\b/
|
|
53
|
+
|
|
54
|
+
const templated_message_date_field_checks = async (): Promise<Check[]> => {
|
|
55
|
+
const checks: Check[] = []
|
|
56
|
+
const tenant = await setup_isolated_tenant({ users: [{ isAdmin: true }] })
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const { sdk } = tenant
|
|
60
|
+
const userId = tenant.users[0].id
|
|
61
|
+
|
|
62
|
+
const fields = Object.fromEntries(DATE_SHAPED_VALUES.map((c, i) => [`DateField${i}`, c.value]))
|
|
63
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
64
|
+
fname: 'Date',
|
|
65
|
+
lname: 'Fields',
|
|
66
|
+
timezone: 'America/New_York', // the timezone the shifted rendering localized into
|
|
67
|
+
fields,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < DATE_SHAPED_VALUES.length; i++) {
|
|
71
|
+
const { label, value } = DATE_SHAPED_VALUES[i]
|
|
72
|
+
const { plaintext } = await sdk.api.templates.get_templated_message({
|
|
73
|
+
enduserId: enduser.id,
|
|
74
|
+
userId,
|
|
75
|
+
channel: 'Chat',
|
|
76
|
+
message: `[{{enduser.DateField${i}}}]`,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
checks.push({
|
|
80
|
+
label: `${label} renders exactly as stored`,
|
|
81
|
+
pass: plaintext === `[${value}]`,
|
|
82
|
+
detail: `stored ${JSON.stringify(value)}, rendered ${JSON.stringify(plaintext)}`,
|
|
83
|
+
})
|
|
84
|
+
// A second, independent signal for the specific symptom, so this still catches a shifted
|
|
85
|
+
// rendering even if the equality check above is ever loosened. Only meaningful for values
|
|
86
|
+
// that carry no time of their own — an ISO datetime legitimately contains one.
|
|
87
|
+
if (!LOOKS_REFORMATTED.test(value)) {
|
|
88
|
+
checks.push({
|
|
89
|
+
label: `${label} does not gain a clock time`,
|
|
90
|
+
pass: !LOOKS_REFORMATTED.test(plaintext),
|
|
91
|
+
detail: `rendered ${JSON.stringify(plaintext)}`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The specific patient-visible symptom, asserted on its own so a failure names it.
|
|
97
|
+
const { plaintext: dob } = await sdk.api.templates.get_templated_message({
|
|
98
|
+
enduserId: enduser.id,
|
|
99
|
+
userId,
|
|
100
|
+
channel: 'Chat',
|
|
101
|
+
message: `DOB: {{enduser.DateField0}}`,
|
|
102
|
+
})
|
|
103
|
+
checks.push({
|
|
104
|
+
label: 'a date of birth does not render as the previous day',
|
|
105
|
+
pass: dob.includes('1985-03-12') && !dob.includes('3/11/1985'),
|
|
106
|
+
detail: `rendered ${JSON.stringify(dob)}`,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Positive control: if templating itself were broken, every check above would pass vacuously
|
|
110
|
+
// (an unreplaced `{{enduser.X}}` is neither reformatted nor time-stamped).
|
|
111
|
+
const { plaintext: control } = await sdk.api.templates.get_templated_message({
|
|
112
|
+
enduserId: enduser.id,
|
|
113
|
+
userId,
|
|
114
|
+
channel: 'Chat',
|
|
115
|
+
message: `{{enduser.fname}} {{enduser.lname}}`,
|
|
116
|
+
})
|
|
117
|
+
checks.push({
|
|
118
|
+
label: 'control: non-date fields are still templated at all',
|
|
119
|
+
pass: control === 'Date Fields',
|
|
120
|
+
detail: `rendered ${JSON.stringify(control)}`,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return checks
|
|
124
|
+
} finally {
|
|
125
|
+
await tenant.teardown()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
130
|
+
export const templated_message_date_field_tests = async () => {
|
|
131
|
+
assert_checks('Templated message date-shaped fields', await templated_message_date_field_checks())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Allow running this test file independently
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
137
|
+
templated_message_date_field_tests()
|
|
138
|
+
.then(() => {
|
|
139
|
+
console.log("✅ Templated message date field test suite completed successfully")
|
|
140
|
+
process.exit(0)
|
|
141
|
+
})
|
|
142
|
+
.catch((error) => {
|
|
143
|
+
console.error("❌ Templated message date field test suite failed:", error)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assert_checks,
|
|
5
|
+
Check,
|
|
6
|
+
setup_isolated_tenant,
|
|
7
|
+
} from "../setup_isolated"
|
|
8
|
+
|
|
9
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Link templating through the real `templates.get_templated_message` — the same endpoint the worker
|
|
13
|
+
* calls on every outbound email and SMS.
|
|
14
|
+
*
|
|
15
|
+
* The exhaustive coverage of this path lives in the API's own module test
|
|
16
|
+
* (`packages/private/api/api/modules/message_templating.test.ts`), which stubs the database so a case
|
|
17
|
+
* costs microseconds. This suite exists to prove those stubs still describe the real thing: that a
|
|
18
|
+
* rendered link really is a `/r/<token>` redirect, and that `linkOpenTrackingIds` really is one id per
|
|
19
|
+
* redirect record created.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately small. Every case here writes real `url_redirects` rows, and `teardown_tenant` does not
|
|
22
|
+
* remove them — that collection is in neither `schema` nor `getInternalQueries()` — so each extra case
|
|
23
|
+
* leaves rows behind in the database forever.
|
|
24
|
+
*
|
|
25
|
+
* Runs standalone or as part of the main suite:
|
|
26
|
+
* npm run test:modular -- api_tests/templated_message_links
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** A tracked link is `<portal or api url>/r/<token>`; the token itself is not predictable. */
|
|
30
|
+
const REDIRECT = /https?:\/\/[^\s"'<>]+\/r\/[A-Za-z0-9_-]+/
|
|
31
|
+
/** Bound on the one case that used to hang the request, so a regression fails instead of stalling. */
|
|
32
|
+
const CLIENT_WAIT_MS = 20_000
|
|
33
|
+
const LINK = 'https://example.com/landing'
|
|
34
|
+
|
|
35
|
+
const templated_message_link_checks = async (): Promise<Check[]> => {
|
|
36
|
+
const checks: Check[] = []
|
|
37
|
+
const tenant = await setup_isolated_tenant({ users: [{ isAdmin: true }] })
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const { sdk } = tenant
|
|
41
|
+
const userId = tenant.users[0].id
|
|
42
|
+
const enduser = await sdk.api.endusers.createOne({ fname: 'Link', lname: 'Templating' })
|
|
43
|
+
|
|
44
|
+
const templated = (message: string, html: string) => sdk.api.templates.get_templated_message({
|
|
45
|
+
enduserId: enduser.id, userId, channel: 'Email', message, html,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 1. one link: an anchor in html, "text (url)" in plaintext, one tracking id
|
|
49
|
+
{
|
|
50
|
+
const body = `Go: {${LINK}}[Click]`
|
|
51
|
+
const { html, plaintext, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
|
|
52
|
+
|
|
53
|
+
checks.push({
|
|
54
|
+
label: 'html renders an anchor whose href is a /r/ redirect',
|
|
55
|
+
pass: new RegExp(`^<p>Go: <a href="${REDIRECT.source}">Click</a></p>$`).test(html),
|
|
56
|
+
detail: `rendered ${JSON.stringify(html)}`,
|
|
57
|
+
})
|
|
58
|
+
checks.push({
|
|
59
|
+
label: 'plaintext appends the same redirect in parentheses',
|
|
60
|
+
pass: new RegExp(`^Go: Click \\(${REDIRECT.source}\\)$`).test(plaintext),
|
|
61
|
+
detail: `rendered ${JSON.stringify(plaintext)}`,
|
|
62
|
+
})
|
|
63
|
+
checks.push({
|
|
64
|
+
label: 'one link creates exactly one redirect record',
|
|
65
|
+
pass: linkOpenTrackingIds.length === 1,
|
|
66
|
+
detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. several links: one record each, all replaced
|
|
71
|
+
{
|
|
72
|
+
const body = `One {${LINK}/a}[A], two {${LINK}/b}[B], three {${LINK}/c}[C]`
|
|
73
|
+
const { html, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
|
|
74
|
+
|
|
75
|
+
checks.push({
|
|
76
|
+
label: 'every link in one message is replaced',
|
|
77
|
+
pass: (html.match(/<a href="/g) || []).length === 3 && !html.includes('{'),
|
|
78
|
+
detail: `rendered ${JSON.stringify(html)}`,
|
|
79
|
+
})
|
|
80
|
+
checks.push({
|
|
81
|
+
label: 'three links create exactly three redirect records',
|
|
82
|
+
pass: linkOpenTrackingIds.length === 3,
|
|
83
|
+
detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. $LINK_ONLY: the bare redirect url, no anchor
|
|
88
|
+
{
|
|
89
|
+
const body = `Go: {${LINK}}[$LINK_ONLY]`
|
|
90
|
+
const { html, plaintext, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
|
|
91
|
+
|
|
92
|
+
checks.push({
|
|
93
|
+
label: '$LINK_ONLY substitutes the bare redirect url',
|
|
94
|
+
pass: new RegExp(`^<p>Go: ${REDIRECT.source}</p>$`).test(html)
|
|
95
|
+
&& new RegExp(`^Go: ${REDIRECT.source}$`).test(plaintext),
|
|
96
|
+
detail: `html ${JSON.stringify(html)}, plaintext ${JSON.stringify(plaintext)}`,
|
|
97
|
+
})
|
|
98
|
+
checks.push({
|
|
99
|
+
label: '$LINK_ONLY still creates its redirect record',
|
|
100
|
+
pass: linkOpenTrackingIds.length === 1,
|
|
101
|
+
detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. a link merge field resolves to the same {url}[text] form, then to an anchor
|
|
106
|
+
{
|
|
107
|
+
const form = await sdk.api.forms.createOne({ title: `Link templating ${Date.now()}` })
|
|
108
|
+
try {
|
|
109
|
+
const body = `Start here: {{forms.${form.id}.link:Begin}}`
|
|
110
|
+
const { html, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
|
|
111
|
+
|
|
112
|
+
checks.push({
|
|
113
|
+
label: 'a form link merge field becomes an anchor on a redirect',
|
|
114
|
+
pass: new RegExp(`^<p>Start here: <a href="${REDIRECT.source}">Begin</a></p>$`).test(html),
|
|
115
|
+
detail: `rendered ${JSON.stringify(html)}`,
|
|
116
|
+
})
|
|
117
|
+
checks.push({
|
|
118
|
+
label: 'the form link creates exactly one redirect record',
|
|
119
|
+
pass: linkOpenTrackingIds.length === 1,
|
|
120
|
+
detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
|
|
121
|
+
})
|
|
122
|
+
} finally {
|
|
123
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 5. The loop-safety case, end to end. A space between the two halves of a link template is one
|
|
128
|
+
// find_link matches but the replacement key cannot, and this request used to never come back:
|
|
129
|
+
// the server looped on that same match, inserting a url_redirects row every time round. The
|
|
130
|
+
// client-side bound is what turns a regression here into a failure rather than a hung suite.
|
|
131
|
+
{
|
|
132
|
+
const body = `Go: {${LINK}} [Click]`
|
|
133
|
+
const outcome = await Promise.race([
|
|
134
|
+
templated(body, `<p>${body}</p>`).then(r => r).catch(e => ({ error: e?.message ?? String(e) } as any)),
|
|
135
|
+
new Promise<any>(resolve => setTimeout(() => resolve({ timedOut: true }), CLIENT_WAIT_MS)),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
checks.push({
|
|
139
|
+
label: 'a template the server cannot replace still returns a response',
|
|
140
|
+
pass: !outcome.timedOut && !outcome.error,
|
|
141
|
+
detail: outcome.timedOut
|
|
142
|
+
? `no response within ${CLIENT_WAIT_MS}ms — the templating loop stopped terminating`
|
|
143
|
+
: outcome.error ? `errored: ${outcome.error}` : 'responded',
|
|
144
|
+
})
|
|
145
|
+
if (!outcome.timedOut && !outcome.error) {
|
|
146
|
+
checks.push({
|
|
147
|
+
label: 'it is left literal in both the html and the plaintext',
|
|
148
|
+
pass: outcome.html === `<p>${body}</p>` && outcome.plaintext === body,
|
|
149
|
+
detail: `html ${JSON.stringify(outcome.html)}, plaintext ${JSON.stringify(outcome.plaintext)}`,
|
|
150
|
+
})
|
|
151
|
+
checks.push({
|
|
152
|
+
label: 'and it creates no redirect records at all',
|
|
153
|
+
pass: outcome.linkOpenTrackingIds.length === 0,
|
|
154
|
+
detail: `linkOpenTrackingIds=${JSON.stringify(outcome.linkOpenTrackingIds)}`,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Positive control: a body with no link syntax is left alone and creates nothing.
|
|
160
|
+
{
|
|
161
|
+
const { html, linkOpenTrackingIds } = await templated('No links here', '<p>No links here</p>')
|
|
162
|
+
checks.push({
|
|
163
|
+
label: 'control: a body without link syntax is untouched and creates no records',
|
|
164
|
+
pass: html === '<p>No links here</p>' && linkOpenTrackingIds.length === 0,
|
|
165
|
+
detail: `rendered ${JSON.stringify(html)}, linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return checks
|
|
170
|
+
} finally {
|
|
171
|
+
await tenant.teardown()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
176
|
+
export const templated_message_link_tests = async () => {
|
|
177
|
+
assert_checks('Templated message links', await templated_message_link_checks())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Allow running this test file independently
|
|
181
|
+
if (require.main === module) {
|
|
182
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
183
|
+
templated_message_link_tests()
|
|
184
|
+
.then(() => {
|
|
185
|
+
console.log("✅ Templated message link test suite completed successfully")
|
|
186
|
+
process.exit(0)
|
|
187
|
+
})
|
|
188
|
+
.catch((error) => {
|
|
189
|
+
console.error("❌ Templated message link test suite failed:", error)
|
|
190
|
+
process.exit(1)
|
|
191
|
+
})
|
|
192
|
+
}
|
|
@@ -6,7 +6,7 @@ import { setup_tests } from "../setup"
|
|
|
6
6
|
|
|
7
7
|
const log = console.log
|
|
8
8
|
|
|
9
|
-
const host = process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
|
|
9
|
+
const host = process.env.API_URL || process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
|
|
10
10
|
|
|
11
11
|
// Helper to assert that an async function throws an error
|
|
12
12
|
const assert_throws = async (fn: () => Promise<any>, description: string) => {
|