@tellescope/sdk 1.256.16 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +3 -1
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -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_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/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 +180 -166
- 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 +6 -3
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -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_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/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 +180 -166
- 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 +5 -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_login_rate_limits.test.ts +7 -7
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +4 -4
- 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_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/allowed-paths-confinement.test.ts +1 -1
- 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/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 +10 -2
- package/src/tests/webhooks_tests.ts +4 -2
- package/test_generated.pdf +0 -0
- package/tsconfig.cjs.json +17 -0
|
@@ -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) => {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { setup_tests } from "../setup"
|
|
11
11
|
|
|
12
12
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
13
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
14
14
|
|
|
15
15
|
// Main test function that can be called independently
|
|
16
16
|
export const user_portal_settings_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { dateValidator, dateOptionalOrEmptyStringValidator, dateValidatorOptional, slugValidator, urlValidator } from "@tellescope/validation"
|
|
2
|
+
|
|
3
|
+
// Pins the accept-sets of the validators whose behavior is delegated to the `validator` package, so
|
|
4
|
+
// a dependency upgrade cannot move what the API accepts without a test failing.
|
|
5
|
+
//
|
|
6
|
+
// These assertions describe PRODUCTION — deliberately, including the parts that are wrong. Two of
|
|
7
|
+
// them are: a bare `YYYY-MM-DD` is rejected while an ISO datetime is accepted, and an unparseable
|
|
8
|
+
// string is accepted and persisted as an `Invalid Date`. Both predate this work and both are
|
|
9
|
+
// API-visible across ~135 schema fields, so changing them belongs in its own PR. Pinning them is
|
|
10
|
+
// what makes this file useful: it is proof of how the deployed API behaves, not an aspiration.
|
|
11
|
+
//
|
|
12
|
+
// Why it was needed. Validator 13.11.0's `isDate` was timezone-dependent — its last step compared a
|
|
13
|
+
// UTC-parsed date against a local-time `.getDate()` — so it answered false west of Greenwich and
|
|
14
|
+
// true in UTC. The servers run UTC, so the behavior pinned below is what production has been doing,
|
|
15
|
+
// while the same code on a US developer's machine did the opposite. 13.12.0 fixed that (#2257), and
|
|
16
|
+
// 13.15.35 was verified to return exactly what 13.11.0 returned under UTC, across 26 inputs and four
|
|
17
|
+
// timezones. So every assertion here holds in EVERY timezone — that is the point of the file, and
|
|
18
|
+
// the reason it can be trusted as a statement about production while running on a laptop.
|
|
19
|
+
//
|
|
20
|
+
// The `isSlug` half is different: 13.15.35 genuinely narrowed it (lowercase-only, alphanumeric ends,
|
|
21
|
+
// 1-character minimum), which would have moved the subdomain accept-set. `validation.ts` pins
|
|
22
|
+
// 13.11.0's pattern inline instead, and the checks below are that pattern.
|
|
23
|
+
//
|
|
24
|
+
// The `isURL` half (section 5) is neither: 13.11.0 and 13.15.35 were compared across 37 URL inputs
|
|
25
|
+
// and agreed on every one, so nothing was at risk in that upgrade and nothing needed pinning inline.
|
|
26
|
+
// It is here as a forward guard — webhook destinations are customer-configured and an accept-set that
|
|
27
|
+
// narrows silently breaks live integrations — and because the staging QA asserted this behavior with
|
|
28
|
+
// no test behind it. `isURL` reads no clock, so that section has no timezone caveat.
|
|
29
|
+
//
|
|
30
|
+
// Pure-function test — no Session needed. Runs as part of the main suite and standalone:
|
|
31
|
+
// npm run test:modular -- api_tests/validation_behavior_locks
|
|
32
|
+
// TZ=UTC npm run test:modular -- api_tests/validation_behavior_locks (must give the same result)
|
|
33
|
+
|
|
34
|
+
// Every assertion runs before anything throws, so one run reports the complete diff rather than the
|
|
35
|
+
// first case that happened to trip.
|
|
36
|
+
const failures: string[] = []
|
|
37
|
+
|
|
38
|
+
const accepts = (label: string, validate: (v: any) => any, value: any) => {
|
|
39
|
+
try { validate(value) }
|
|
40
|
+
catch (err: any) { failures.push(`${label}: expected to ACCEPT ${JSON.stringify(value)}, but it was rejected (${err?.message || err})`) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rejects = (label: string, validate: (v: any) => any, value: any) => {
|
|
44
|
+
let accepted = false
|
|
45
|
+
try { validate(value); accepted = true }
|
|
46
|
+
catch { /* expected */ }
|
|
47
|
+
if (accepted) failures.push(`${label}: expected to REJECT ${JSON.stringify(value)}, but it was accepted`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const validation_behavior_lock_tests = async () => {
|
|
51
|
+
console.log(`Running validation behavior lock tests (TZ=${process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone})`)
|
|
52
|
+
|
|
53
|
+
const date = dateValidator.validate()
|
|
54
|
+
const dateOptional = dateValidatorOptional.validate()
|
|
55
|
+
const dateOrEmpty = dateOptionalOrEmptyStringValidator.validate()
|
|
56
|
+
|
|
57
|
+
// -----------------------------------------------------------------------------------------------
|
|
58
|
+
// 1. Date fields: ISO datetimes are accepted
|
|
59
|
+
// -----------------------------------------------------------------------------------------------
|
|
60
|
+
// What the SDK sends, since JSON.stringify turns a Date into one of these.
|
|
61
|
+
for (const value of [
|
|
62
|
+
'2024-06-15T14:30:00Z',
|
|
63
|
+
'2024-06-15T00:00:00.000Z',
|
|
64
|
+
'2024-06-15T14:30:00+05:00',
|
|
65
|
+
]) {
|
|
66
|
+
accepts('dateValidator', date, value)
|
|
67
|
+
accepts('dateValidatorOptional', dateOptional, value)
|
|
68
|
+
accepts('dateOptionalOrEmptyStringValidator', dateOrEmpty, value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Epoch milliseconds round-trip through `new Date()`.
|
|
72
|
+
accepts('dateValidator', date, 1718400000000)
|
|
73
|
+
|
|
74
|
+
// The empty string is the "cleared" value for the two lenient variants only.
|
|
75
|
+
accepts('dateOptionalOrEmptyStringValidator', dateOrEmpty, '')
|
|
76
|
+
rejects('dateValidator', date, '')
|
|
77
|
+
|
|
78
|
+
// -----------------------------------------------------------------------------------------------
|
|
79
|
+
// 2. Date fields: a bare calendar date is REJECTED
|
|
80
|
+
// -----------------------------------------------------------------------------------------------
|
|
81
|
+
// Surprising, and not a typo. `isDate` returns true for these and the check is inverted, so they
|
|
82
|
+
// 400. This is long-standing production behavior; it is pinned, not endorsed. A client sending
|
|
83
|
+
// `"1985-03-12"` to a date field gets an error and must send an ISO datetime instead.
|
|
84
|
+
for (const value of ['1985-03-12', '2024-01-01', '2026-10-01', '2000-02-29', '1999/12/31']) {
|
|
85
|
+
rejects('dateValidator', date, value)
|
|
86
|
+
rejects('dateValidatorOptional', dateOptional, value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A real Date object is rejected by the same mechanism (isDate is true for one). Nothing sends a
|
|
90
|
+
// Date over HTTP, so this only reaches internal callers — but the three validators disagree here:
|
|
91
|
+
// the `dateOrEmpty` variant coerces with `.toString()` first, producing "Sat Jun 15 2024 ...",
|
|
92
|
+
// which isDate calls false, so it accepts. Pinned because it is a real inconsistency.
|
|
93
|
+
rejects('dateValidator', date, new Date('2024-06-15T14:30:00Z'))
|
|
94
|
+
rejects('dateValidatorOptional', dateOptional, new Date('2024-06-15T14:30:00Z'))
|
|
95
|
+
accepts('dateOptionalOrEmptyStringValidator', dateOrEmpty, new Date('2024-06-15T14:30:00Z'))
|
|
96
|
+
|
|
97
|
+
// -----------------------------------------------------------------------------------------------
|
|
98
|
+
// 3. Date fields: an unparseable string is ACCEPTED and stored as an Invalid Date
|
|
99
|
+
// -----------------------------------------------------------------------------------------------
|
|
100
|
+
// A data-corruption path, pinned so it is documented and so a future fix is a deliberate, visible
|
|
101
|
+
// change rather than a silent one. `new Date('not a date')` is an Invalid Date, and it is what
|
|
102
|
+
// gets written.
|
|
103
|
+
for (const value of ['not a date', 'hello world', 'tomorrow', '??']) {
|
|
104
|
+
accepts('dateValidator', date, value)
|
|
105
|
+
accepts('dateValidatorOptional', dateOptional, value)
|
|
106
|
+
accepts('dateOptionalOrEmptyStringValidator', dateOrEmpty, value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -----------------------------------------------------------------------------------------------
|
|
110
|
+
// 4. Organization subdomain
|
|
111
|
+
// -----------------------------------------------------------------------------------------------
|
|
112
|
+
const slug = slugValidator.validate()
|
|
113
|
+
|
|
114
|
+
// Valid, and must stay valid — 13.15.35's isSlug would reject the mixed-case ones.
|
|
115
|
+
for (const value of ['acme', 'acme-health', 'tellescope-demo', 'demo_org_1', 'Acme', 'My-Org', 'TellescopeDemo', 'abc']) {
|
|
116
|
+
accepts('slugValidator', slug, value)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Also accepted today, though none of these are valid DNS labels. Pinned so the upgrade does not
|
|
120
|
+
// quietly change them; tightening is a separate decision.
|
|
121
|
+
for (const value of ['acme.health', 'org/sub', 'my@org', 'org?x=1']) {
|
|
122
|
+
accepts('slugValidator', slug, value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Rejected today — note 13.15.35's isSlug would ACCEPT the 1- and 2-character cases.
|
|
126
|
+
for (const value of ['a', 'ab', 'my--org', 'my__org', '-acme', 'acme-', 'org name']) {
|
|
127
|
+
rejects('slugValidator', slug, value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// -----------------------------------------------------------------------------------------------
|
|
131
|
+
// 5. Webhook and other URL fields
|
|
132
|
+
// -----------------------------------------------------------------------------------------------
|
|
133
|
+
// `urlValidator` is a thin `isURL` passthrough behind `build_validator`, used for webhook
|
|
134
|
+
// subscriptions (schema `webhooks.url`) among others. It is a SYNTAX check only — refusing internal
|
|
135
|
+
// destinations is a separate layer (`api/modules/outbound_url.ts`), which is why several things
|
|
136
|
+
// accepted here are refused at delivery time.
|
|
137
|
+
const url = urlValidator.validate()
|
|
138
|
+
|
|
139
|
+
// Ports and query strings, the cases the staging QA calls out.
|
|
140
|
+
for (const value of [
|
|
141
|
+
'https://example.com:8443/hook',
|
|
142
|
+
'https://example.com/hook?a=1&b=two',
|
|
143
|
+
'https://example.com:8443/hook?a=1',
|
|
144
|
+
'https://example.com/a?b=c&d=%7B%22x%22%3A1%7D', // percent-encoded JSON in the query
|
|
145
|
+
'https://example.com:443/hook',
|
|
146
|
+
'https://example.com:8443', // port, no path
|
|
147
|
+
'https://example.com/hook#frag',
|
|
148
|
+
'HTTPS://EXAMPLE.COM/Hook',
|
|
149
|
+
'https://[::1]:8443/hook',
|
|
150
|
+
]) {
|
|
151
|
+
accepts('urlValidator', url, value)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Accepted, but quirks rather than intent — pinned like the date quirks above so that tightening any
|
|
155
|
+
// of them is a deliberate, visible change.
|
|
156
|
+
// - schemeless: values of this shape exist in the webhooks collection today (outbound_url.ts:213)
|
|
157
|
+
// - ftp: isURL's default protocol list is http/https/ftp
|
|
158
|
+
// - the last two are refused by assertPublicHttpUrl at delivery, not here
|
|
159
|
+
for (const value of [
|
|
160
|
+
'api.customer.com/hook',
|
|
161
|
+
'ftp://example.com/x',
|
|
162
|
+
'http://10.0.0.1/hook',
|
|
163
|
+
'https://user:pass@example.com/hook',
|
|
164
|
+
]) {
|
|
165
|
+
accepts('urlValidator', url, value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Rejected. `localhost` fails isURL's require_tld, which defaults on — a local webhook receiver has
|
|
169
|
+
// to be configured as 127.0.0.1, and that is why the SSRF suite binds loopback by address.
|
|
170
|
+
for (const value of [
|
|
171
|
+
'http://localhost:8080/hook',
|
|
172
|
+
'localhost:8080/hook',
|
|
173
|
+
'https://example.com:65536/hook',
|
|
174
|
+
'https://example.com:0/hook',
|
|
175
|
+
'https://example.com:abc/x',
|
|
176
|
+
'javascript:alert(1)',
|
|
177
|
+
'file:///etc/passwd',
|
|
178
|
+
'mailto:a@b.com',
|
|
179
|
+
'//example.com/hook',
|
|
180
|
+
'not a url',
|
|
181
|
+
'https://example.com/hook with space',
|
|
182
|
+
'https://example.com./hook', // trailing dot
|
|
183
|
+
]) {
|
|
184
|
+
rejects('urlValidator', url, value)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Non-string and absent values. The empty string is rejected by build_validator's non-empty rule
|
|
188
|
+
// rather than by isURL, so it is pinned separately from the malformed strings above.
|
|
189
|
+
rejects('urlValidator', url, '')
|
|
190
|
+
rejects('urlValidator', url, 123)
|
|
191
|
+
rejects('urlValidator', url, null)
|
|
192
|
+
rejects('urlValidator', url, undefined)
|
|
193
|
+
|
|
194
|
+
if (failures.length) {
|
|
195
|
+
throw new Error(`${failures.length} validation behavior lock(s) broken:\n - ` + failures.join('\n - '))
|
|
196
|
+
}
|
|
197
|
+
console.log("✅ validation behavior lock tests passed")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (require.main === module) {
|
|
201
|
+
validation_behavior_lock_tests()
|
|
202
|
+
.then(() => { console.log("✅ suite completed"); process.exit(0) })
|
|
203
|
+
.catch((err) => { console.error("❌ suite failed:", err); process.exit(1) })
|
|
204
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@tellescope/utilities"
|
|
10
10
|
import { Session, /* APIQuery */ } from "../sdk"
|
|
11
11
|
import { PublicEndpoints } from "../public"
|
|
12
|
+
import { reset_test_tenant } from "./setup"
|
|
12
13
|
|
|
13
14
|
const host = process.env.API_URL || 'http://localhost:8080'
|
|
14
15
|
const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
|
|
@@ -18,7 +19,7 @@ const sdkPub = PublicEndpoints({ host })
|
|
|
18
19
|
|
|
19
20
|
if (!(email && password)) {
|
|
20
21
|
console.error("Set TEST_EMAIL and TEST_PASSWORD")
|
|
21
|
-
process.exit()
|
|
22
|
+
process.exit(1) // 1, not 0: an unconfigured run must not report success
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const enduser_login_tests = async () => {
|
|
@@ -59,7 +60,7 @@ const enduser_login_tests = async () => {
|
|
|
59
60
|
|
|
60
61
|
try {
|
|
61
62
|
await sdk.authenticate(email, password)
|
|
62
|
-
await sdk
|
|
63
|
+
await reset_test_tenant(sdk)
|
|
63
64
|
|
|
64
65
|
await enduser_login_tests()
|
|
65
66
|
} catch(err) {
|