@tellescope/sdk 2.0.0-beta.0 → 2.0.0-beta.2
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 +24 -0
- package/env.example +6 -0
- package/lib/cjs/sdk.d.ts +376 -3
- 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/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.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/organization_signup.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/programs.test.js +73 -17
- package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- 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 +58 -48
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/cjs/tests/setup_isolated.d.ts +48 -0
- package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
- package/lib/cjs/tests/setup_isolated.js +119 -1
- package/lib/cjs/tests/setup_isolated.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +378 -282
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +44 -34
- package/lib/cjs/tests/webhooks_tests.js.map +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/esm/sdk.d.ts +376 -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/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.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/organization_signup.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
- package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/programs.test.js +72 -16
- package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- 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 +57 -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/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/esm/tests/setup_isolated.d.ts +48 -0
- package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
- package/lib/esm/tests/setup_isolated.js +114 -0
- package/lib/esm/tests/setup_isolated.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +379 -283
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +44 -34
- package/lib/esm/tests/webhooks_tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -11
- package/src/sdk.ts +9 -2
- package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
- package/src/tests/api_tests/enduser_fields_sync.test.ts +154 -0
- package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
- package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
- package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
- package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
- package/src/tests/api_tests/load_threads_care_team.test.ts +214 -0
- package/src/tests/api_tests/mdi_case_offerings.test.ts +138 -0
- package/src/tests/api_tests/organization_signup.test.ts +4 -0
- package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
- package/src/tests/api_tests/programs.test.ts +351 -0
- package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
- package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +37 -26
- package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +436 -0
- package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
- package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
- package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
- package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
- package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
- package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
- package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
- package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
- package/src/tests/api_tests/totp_mfa.test.ts +10 -5
- package/src/tests/api_tests/trigger_contains_matching.test.ts +349 -0
- package/src/tests/api_tests/user_logs_restore_deleted.test.ts +419 -0
- package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
- package/src/tests/api_tests/vitl_push.test.ts +323 -0
- package/src/tests/setup_isolated.ts +140 -0
- package/src/tests/tests.ts +126 -20
- package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
- package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
- package/src/tests/unit_tests/trigger_title_partials.test.ts +174 -0
- package/src/tests/webhooks_tests.ts +6 -1
- package/test_generated.pdf +0 -0
|
@@ -65,6 +65,19 @@ const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
|
65
65
|
* R14 both grace markers are load-bearing: neither source nor stripeCustomerId alone opens it
|
|
66
66
|
* R15 the grace is per-request — it does not clear the stored flag
|
|
67
67
|
*
|
|
68
|
+
* ?alwaysSetPhone=true (CU-86e3k4ptv) is a second per-request override, narrower than ?enduserId=:
|
|
69
|
+
* it allows PHONE ONLY through the lock, on every finalized webhook, overwriting an existing and
|
|
70
|
+
* different number. Email — the password-reset channel this lock exists to protect — stays blocked.
|
|
71
|
+
* G16 (GREEN) the flag overwrites an existing, different phone on a guessed match
|
|
72
|
+
* G17 (GREEN) ...and in that same request the submitted email is still dropped
|
|
73
|
+
* R16 (RED) the identical request WITHOUT the flag leaves both phone and email blocked
|
|
74
|
+
* R17 (RED) the flag does not open email on a phone match either
|
|
75
|
+
* R18 (RED) only the literal string 'true' enables it — ?alwaysSetPhone=1 keeps the lock
|
|
76
|
+
* G18 (GREEN) a flagged webhook still stamps authFieldIntakeDisabled on the response, so later
|
|
77
|
+
* writes through save_enduser_updates_for_form_response stay blocked
|
|
78
|
+
* R19 (RED) it is request-scoped: a flag passed on an earlier webhook for the same
|
|
79
|
+
* responder_uuid does not unlock a later one that omits it
|
|
80
|
+
*
|
|
68
81
|
* The Stripe grace is deliberately confined to THIS endpoint, which is API-key authenticated. The
|
|
69
82
|
* anonymous public-form path gets no exemption; that boundary is pinned by G28 in
|
|
70
83
|
* public_endpoint_auth_field_lock.test.ts.
|
|
@@ -155,12 +168,15 @@ export const formsort_webhook_auth_field_lock_tests = async (
|
|
|
155
168
|
finalized: boolean,
|
|
156
169
|
enduserId?: string,
|
|
157
170
|
createNewEnduser?: boolean,
|
|
171
|
+
// sent verbatim, so a scenario can pin that only the literal string 'true' enables the opt-in
|
|
172
|
+
alwaysSetPhone?: string,
|
|
158
173
|
}) => {
|
|
159
174
|
const url = new URL(`${host}/v1/webhooks/form-ingestion/${apiKey.key}`)
|
|
160
175
|
url.searchParams.set('formId', form.id)
|
|
161
176
|
url.searchParams.set('returnJSON', 'true')
|
|
162
177
|
if (o.enduserId) url.searchParams.set('enduserId', o.enduserId)
|
|
163
178
|
if (o.createNewEnduser) url.searchParams.set('createNewEnduser', 'true')
|
|
179
|
+
if (o.alwaysSetPhone !== undefined) url.searchParams.set('alwaysSetPhone', o.alwaysSetPhone)
|
|
164
180
|
|
|
165
181
|
const res = await axios.post(
|
|
166
182
|
url.toString(),
|
|
@@ -688,6 +704,215 @@ export const formsort_webhook_auth_field_lock_tests = async (
|
|
|
688
704
|
}
|
|
689
705
|
})
|
|
690
706
|
|
|
707
|
+
// ---------------------------------------------------------------------------------------------
|
|
708
|
+
// G16 — ?alwaysSetPhone=true: the opt-in allows phone through the lock on a GUESSED match (the
|
|
709
|
+
// path R2 blocks), overwriting a phone that is already stored and different. Phone is also the
|
|
710
|
+
// only field this submission changes, which exercises the "at least one update provided" guard —
|
|
711
|
+
// if the delete happened before that guard, nothing would be written at all.
|
|
712
|
+
// ---------------------------------------------------------------------------------------------
|
|
713
|
+
await runScenario('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match', async () => {
|
|
714
|
+
const email = emailFor('g16')
|
|
715
|
+
const originalPhone = nextPhone()
|
|
716
|
+
const victim = await makeVictim('g16', { email, phone: originalPhone })
|
|
717
|
+
const intakePhone = nextPhone()
|
|
718
|
+
|
|
719
|
+
const { data } = await postWebhook({
|
|
720
|
+
label: 'g16',
|
|
721
|
+
finalized: true,
|
|
722
|
+
alwaysSetPhone: 'true',
|
|
723
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
|
|
724
|
+
})
|
|
725
|
+
if (!matched('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match', data, victim.id)) return
|
|
726
|
+
|
|
727
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
|
|
728
|
+
record('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match',
|
|
729
|
+
enduser?.phone === intakePhone,
|
|
730
|
+
`phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------------------------
|
|
734
|
+
// G17 — the same request, with BOTH a new phone and a different email, so the two halves of the
|
|
735
|
+
// rule are observed together: phone lands, email does not. Shaped like R3 (a draft formed by a
|
|
736
|
+
// guess, finalized through the sticky flag) because that is the only resolution path where the
|
|
737
|
+
// submitted email can differ from the stored one while the patient still matches.
|
|
738
|
+
// ---------------------------------------------------------------------------------------------
|
|
739
|
+
await runScenario('G17', 'alwaysSetPhone=true writes phone and still drops email', async () => {
|
|
740
|
+
const originalEmail = emailFor('g17')
|
|
741
|
+
const originalPhone = nextPhone()
|
|
742
|
+
const victim = await makeVictim('g17', { email: originalEmail, phone: originalPhone })
|
|
743
|
+
const attackerEmail = emailFor('g17-attacker')
|
|
744
|
+
const intakePhone = nextPhone()
|
|
745
|
+
|
|
746
|
+
// draft matches the pre-existing patient by phone -> authFieldIntakeDisabled is stamped on it
|
|
747
|
+
await postWebhook({ label: 'g17', finalized: false, answers: [{ key: 'phone', value: originalPhone }] })
|
|
748
|
+
|
|
749
|
+
const { data } = await postWebhook({
|
|
750
|
+
label: 'g17',
|
|
751
|
+
finalized: true,
|
|
752
|
+
alwaysSetPhone: 'true',
|
|
753
|
+
answers: [{ key: 'phone', value: intakePhone }, { key: 'email', value: attackerEmail }],
|
|
754
|
+
})
|
|
755
|
+
if (!matched('G17', 'alwaysSetPhone=true writes phone and still drops email', data, victim.id)) return
|
|
756
|
+
|
|
757
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
|
|
758
|
+
record('G17', 'alwaysSetPhone=true writes phone through the sticky flag', enduser?.phone === intakePhone,
|
|
759
|
+
`phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
|
|
760
|
+
record('G17', 'alwaysSetPhone=true leaves email blocked in the same request',
|
|
761
|
+
enduser?.email === originalEmail,
|
|
762
|
+
`email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// ---------------------------------------------------------------------------------------------
|
|
766
|
+
// R16 — the control for G17: byte-identical request without the flag. Both fields stay blocked,
|
|
767
|
+
// i.e. the default behavior is unchanged.
|
|
768
|
+
// ---------------------------------------------------------------------------------------------
|
|
769
|
+
await runScenario('R16', 'without the flag the same request blocks phone and email', async () => {
|
|
770
|
+
const originalEmail = emailFor('r16')
|
|
771
|
+
const originalPhone = nextPhone()
|
|
772
|
+
const victim = await makeVictim('r16', { email: originalEmail, phone: originalPhone })
|
|
773
|
+
const attackerEmail = emailFor('r16-attacker')
|
|
774
|
+
const intakePhone = nextPhone()
|
|
775
|
+
|
|
776
|
+
await postWebhook({ label: 'r16', finalized: false, answers: [{ key: 'phone', value: originalPhone }] })
|
|
777
|
+
|
|
778
|
+
const { data } = await postWebhook({
|
|
779
|
+
label: 'r16',
|
|
780
|
+
finalized: true,
|
|
781
|
+
answers: [{ key: 'phone', value: intakePhone }, { key: 'email', value: attackerEmail }],
|
|
782
|
+
})
|
|
783
|
+
if (!matched('R16', 'without the flag the same request blocks phone and email', data, victim.id)) return
|
|
784
|
+
|
|
785
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone || e.email === attackerEmail)
|
|
786
|
+
record('R16', 'without the flag phone stays blocked', enduser?.phone === originalPhone,
|
|
787
|
+
`phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
|
|
788
|
+
record('R16', 'without the flag email stays blocked', enduser?.email === originalEmail,
|
|
789
|
+
`email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
// ---------------------------------------------------------------------------------------------
|
|
793
|
+
// R17 — the flag is phone-only in BOTH match directions: matched on phone (R1's shape), the
|
|
794
|
+
// submitted email must still not land.
|
|
795
|
+
// ---------------------------------------------------------------------------------------------
|
|
796
|
+
await runScenario('R17', 'alwaysSetPhone=true does not repoint email on a phone match', async () => {
|
|
797
|
+
const originalEmail = emailFor('r17')
|
|
798
|
+
const phone = nextPhone()
|
|
799
|
+
const victim = await makeVictim('r17', { email: originalEmail, phone })
|
|
800
|
+
const attackerEmail = emailFor('r17-attacker')
|
|
801
|
+
|
|
802
|
+
const { data } = await postWebhook({
|
|
803
|
+
label: 'r17',
|
|
804
|
+
finalized: true,
|
|
805
|
+
alwaysSetPhone: 'true',
|
|
806
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
807
|
+
})
|
|
808
|
+
if (!matched('R17', 'alwaysSetPhone=true does not repoint email on a phone match', data, victim.id)) return
|
|
809
|
+
|
|
810
|
+
const enduser = await pollEnduser(victim.id, e => e.email === attackerEmail)
|
|
811
|
+
record('R17', 'alwaysSetPhone=true does not repoint email on a phone match', enduser?.email === originalEmail,
|
|
812
|
+
`email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
// ---------------------------------------------------------------------------------------------
|
|
816
|
+
// R18 — parsed as === 'true', matching createNewEnduser / matchByName. Any other value, including
|
|
817
|
+
// a truthy-looking one, keeps today's behavior.
|
|
818
|
+
// ---------------------------------------------------------------------------------------------
|
|
819
|
+
await runScenario('R18', 'alwaysSetPhone=1 does not enable the opt-in', async () => {
|
|
820
|
+
const email = emailFor('r18')
|
|
821
|
+
const originalPhone = nextPhone()
|
|
822
|
+
const victim = await makeVictim('r18', { email, phone: originalPhone })
|
|
823
|
+
const intakePhone = nextPhone()
|
|
824
|
+
|
|
825
|
+
const { data } = await postWebhook({
|
|
826
|
+
label: 'r18',
|
|
827
|
+
finalized: true,
|
|
828
|
+
alwaysSetPhone: '1',
|
|
829
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
|
|
830
|
+
})
|
|
831
|
+
if (!matched('R18', 'alwaysSetPhone=1 does not enable the opt-in', data, victim.id)) return
|
|
832
|
+
|
|
833
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
|
|
834
|
+
record('R18', 'alwaysSetPhone=1 does not enable the opt-in', enduser?.phone === originalPhone,
|
|
835
|
+
`phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
// ---------------------------------------------------------------------------------------------
|
|
839
|
+
// G18 — the opt-in is a per-REQUEST allow, exactly like G11 (?enduserId=) and R15 (the Stripe
|
|
840
|
+
// grace): the response still records that it attached to a pre-existing patient, so the other
|
|
841
|
+
// consumer of that flag — save_enduser_updates_for_form_response, reached by later staff edits —
|
|
842
|
+
// stays protected.
|
|
843
|
+
// ---------------------------------------------------------------------------------------------
|
|
844
|
+
await runScenario('G18', 'alwaysSetPhone does not clear the stored flag', async () => {
|
|
845
|
+
const email = emailFor('g18')
|
|
846
|
+
const victim = await makeVictim('g18', { email, phone: nextPhone() })
|
|
847
|
+
const intakePhone = nextPhone()
|
|
848
|
+
|
|
849
|
+
const { data } = await postWebhook({
|
|
850
|
+
label: 'g18',
|
|
851
|
+
finalized: true,
|
|
852
|
+
alwaysSetPhone: 'true',
|
|
853
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
|
|
854
|
+
})
|
|
855
|
+
if (!matched('G18', 'alwaysSetPhone does not clear the stored flag', data, victim.id)) return
|
|
856
|
+
|
|
857
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
|
|
858
|
+
record('G18', 'flagged webhook landed the phone (probe precondition)', enduser?.phone === intakePhone,
|
|
859
|
+
`phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
|
|
860
|
+
|
|
861
|
+
if (!data.formResponseId) {
|
|
862
|
+
skip('G18', 'alwaysSetPhone does not clear the stored flag', 'webhook returned no formResponseId')
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
const response: any = await sdk.api.form_responses.getOne(data.formResponseId).catch(() => null)
|
|
866
|
+
if (!response) {
|
|
867
|
+
skip('G18', 'alwaysSetPhone does not clear the stored flag', 'form response not readable')
|
|
868
|
+
} else if (response.authFieldIntakeDisabled === undefined) {
|
|
869
|
+
// deliberately absent from schema.ts; if reads project it away there is nothing to assert here
|
|
870
|
+
skip('G18', 'alwaysSetPhone does not clear the stored flag', 'authFieldIntakeDisabled not exposed on read')
|
|
871
|
+
} else {
|
|
872
|
+
record('G18', 'alwaysSetPhone does not clear the stored flag', response.authFieldIntakeDisabled === true,
|
|
873
|
+
`authFieldIntakeDisabled=${response.authFieldIntakeDisabled} expected=true`)
|
|
874
|
+
}
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
// ---------------------------------------------------------------------------------------------
|
|
878
|
+
// R19 — nothing about the opt-in is recorded on the response, so a later webhook for the same
|
|
879
|
+
// responder_uuid that omits it is blocked as usual. The earlier webhook here must be the DRAFT:
|
|
880
|
+
// once a response is submitted the handler returns 'Already submitted' before reaching the lock,
|
|
881
|
+
// which would make a finalized-then-finalized pair pass no matter what the implementation did.
|
|
882
|
+
// ---------------------------------------------------------------------------------------------
|
|
883
|
+
await runScenario('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', async () => {
|
|
884
|
+
const email = emailFor('r19')
|
|
885
|
+
const originalPhone = nextPhone()
|
|
886
|
+
const victim = await makeVictim('r19', { email, phone: originalPhone })
|
|
887
|
+
const intakePhone = nextPhone()
|
|
888
|
+
|
|
889
|
+
// draft WITH the flag: matches the pre-existing patient by email and stamps the response
|
|
890
|
+
const draft = await postWebhook({
|
|
891
|
+
label: 'r19',
|
|
892
|
+
finalized: false,
|
|
893
|
+
alwaysSetPhone: 'true',
|
|
894
|
+
answers: [{ key: 'email', value: email }],
|
|
895
|
+
})
|
|
896
|
+
if (draft.status !== 200) {
|
|
897
|
+
record('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', false,
|
|
898
|
+
`draft webhook returned ${draft.status}`)
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// finalize WITHOUT the flag — if the opt-in were inherited from the response, this would land
|
|
903
|
+
const { data } = await postWebhook({
|
|
904
|
+
label: 'r19',
|
|
905
|
+
finalized: true,
|
|
906
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
|
|
907
|
+
})
|
|
908
|
+
if (!matched('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', data, victim.id)) return
|
|
909
|
+
|
|
910
|
+
const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
|
|
911
|
+
record('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one',
|
|
912
|
+
enduser?.phone === originalPhone,
|
|
913
|
+
`phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
|
|
914
|
+
})
|
|
915
|
+
|
|
691
916
|
// ---------------------------------------------------------------------------------------------
|
|
692
917
|
// Report
|
|
693
918
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -65,7 +65,7 @@ export const register_business_id_injection_tests = async ({ sdk } : { sdk: Sess
|
|
|
65
65
|
// An anonymous caller registers with businessId set to the victim org. On vulnerable
|
|
66
66
|
// code this creates a Business user INSIDE the victim org that no one invited.
|
|
67
67
|
// ==================================================================================
|
|
68
|
-
const email1 = `
|
|
68
|
+
const email1 = `dontsend+reg-inject-${Date.now()}@tellescope.com`
|
|
69
69
|
|
|
70
70
|
const before = await sdk.api.users.getOne({ email: email1 }).catch(() => null)
|
|
71
71
|
assert(before === null, `precondition failed: ${email1} already present in victim org`, 'precondition: victim org has no such user')
|
|
@@ -95,7 +95,7 @@ export const register_business_id_injection_tests = async ({ sdk } : { sdk: Sess
|
|
|
95
95
|
// businessId. On vulnerable code the scoped duplicate check misses the org-less record,
|
|
96
96
|
// so a SECOND user record for the same email is created inside the victim org.
|
|
97
97
|
// ==================================================================================
|
|
98
|
-
const email2 = `
|
|
98
|
+
const email2 = `dontsend+reg-dup-${Date.now()}@tellescope.com`
|
|
99
99
|
|
|
100
100
|
const resRoot = await post('/v1/users/register', { email: email2, password, termsVersion })
|
|
101
101
|
console.log(` register(no businessId) -> HTTP ${resRoot.status}`)
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
assert_checks,
|
|
6
6
|
Check,
|
|
7
7
|
setup_isolated_tenant,
|
|
8
|
+
IsolatedSuite,
|
|
8
9
|
} from "../../setup_isolated"
|
|
9
10
|
|
|
10
11
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
@@ -182,6 +183,9 @@ export const rich_text_answer_sanitized_on_write_tests = async () => {
|
|
|
182
183
|
assert_checks('Rich Text answers sanitized on write', await rich_text_answer_sanitized_on_write_checks())
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
|
|
187
|
+
export const ISOLATED_SUITE: IsolatedSuite = { name: 'Rich Text answers sanitized on write', run: rich_text_answer_sanitized_on_write_checks }
|
|
188
|
+
|
|
185
189
|
// Allow running this test file independently
|
|
186
190
|
if (require.main === module) {
|
|
187
191
|
console.log(`🌐 Using API URL: ${host}`)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assert_checks,
|
|
5
|
+
Check,
|
|
6
|
+
setup_isolated_tenant,
|
|
7
|
+
IsolatedSuite,
|
|
8
|
+
} from "../setup_isolated"
|
|
9
|
+
import { EnduserSession } from "../../sdk"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
14
|
+
|
|
15
|
+
// the session is confined to the video-join-link paths, so read the identity from the token itself
|
|
16
|
+
const enduserIdFromAuthToken = (authToken: string): string => (
|
|
17
|
+
JSON.parse(Buffer.from(authToken.split('.')[1], 'base64url').toString()).id
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `calendar_events.session_for_join_link` must log in as the attendee who holds the token, not the
|
|
22
|
+
* first enduser attendee on the event.
|
|
23
|
+
*/
|
|
24
|
+
export const session_for_join_link_checks = async (): Promise<Check[]> => {
|
|
25
|
+
const checks: Check[] = []
|
|
26
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
27
|
+
|
|
28
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
tenant = await setup_isolated_tenant({
|
|
32
|
+
name: `join link ${RAND()}`,
|
|
33
|
+
users: [{ isAdmin: true }],
|
|
34
|
+
apiKeys: 0,
|
|
35
|
+
})
|
|
36
|
+
const { sdk, businessId } = tenant
|
|
37
|
+
|
|
38
|
+
const [enduserA, enduserB] = await Promise.all([
|
|
39
|
+
sdk.api.endusers.createOne({ email: `join_a_${RAND()}@test.tellescope.com` }),
|
|
40
|
+
sdk.api.endusers.createOne({ email: `join_b_${RAND()}@test.tellescope.com` }),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
const tokenA = `join-a-${RAND()}${RAND()}`
|
|
44
|
+
const tokenB = `join-b-${RAND()}${RAND()}`
|
|
45
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
46
|
+
title: 'join link', durationInMinutes: 30, startTimeInMS: Date.now() + 3600_000,
|
|
47
|
+
attendees: [
|
|
48
|
+
{ type: 'enduser', id: enduserA.id, joinLinkToken: tokenA },
|
|
49
|
+
{ type: 'enduser', id: enduserB.id, joinLinkToken: tokenB },
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const sessionFor = (token: string) => (
|
|
54
|
+
new EnduserSession({ host, businessId }).api.calendar_events.session_for_join_link({ token })
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const resultB = await sessionFor(tokenB)
|
|
58
|
+
const idB = enduserIdFromAuthToken(resultB.authToken)
|
|
59
|
+
record(
|
|
60
|
+
"second attendee's token logs in as the second attendee",
|
|
61
|
+
idB === enduserB.id && resultB.eventId === event.id,
|
|
62
|
+
`expected ${enduserB.id}, got ${idB}${idB === enduserA.id ? ' (the first attendee)' : ''}`,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const resultA = await sessionFor(tokenA)
|
|
66
|
+
const idA = enduserIdFromAuthToken(resultA.authToken)
|
|
67
|
+
record(
|
|
68
|
+
"first attendee's token logs in as the first attendee",
|
|
69
|
+
idA === enduserA.id && resultA.eventId === event.id,
|
|
70
|
+
`expected ${enduserA.id}, got ${idA}`,
|
|
71
|
+
)
|
|
72
|
+
} finally {
|
|
73
|
+
// teardown before anything asserts — assert calls process.exit, which skips pending finallys
|
|
74
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return checks
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const session_for_join_link_tests = async () => {
|
|
81
|
+
assert_checks('session_for_join_link attendee', await session_for_join_link_checks())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
|
|
85
|
+
export const ISOLATED_SUITE: IsolatedSuite = { name: 'session_for_join_link attendee', run: session_for_join_link_checks }
|
|
86
|
+
|
|
87
|
+
// Allow running this test file independently
|
|
88
|
+
if (require.main === module) {
|
|
89
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
90
|
+
session_for_join_link_tests()
|
|
91
|
+
.then(() => {
|
|
92
|
+
console.log("✅ session_for_join_link test suite completed successfully")
|
|
93
|
+
process.exit(0)
|
|
94
|
+
})
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
console.error("❌ session_for_join_link test suite failed:", error)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
@@ -22,6 +22,7 @@ const buildSetFieldsAction = (prefix: string) => ({
|
|
|
22
22
|
{ name: `${prefix}_tracking`, value: '{{order.tracking}}', type: 'Custom Value' as const },
|
|
23
23
|
{ name: `${prefix}_carrier`, value: '{{order.carrier}}', type: 'Custom Value' as const },
|
|
24
24
|
{ name: `${prefix}_sku`, value: '{{order.sku}}', type: 'Custom Value' as const },
|
|
25
|
+
{ name: `${prefix}_bookingLink`, value: '{{order.bookingLink}}', type: 'Custom Value' as const },
|
|
25
26
|
{ name: `${prefix}_externalId`, value: '{{order.externalId}}', type: 'Custom Value' as const },
|
|
26
27
|
{ name: `${prefix}_id`, value: '{{order.id}}', type: 'Custom Value' as const },
|
|
27
28
|
{ name: `${prefix}_protocol`, value: '{{order.protocol}}', type: 'Custom Value' as const },
|
|
@@ -55,6 +56,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
55
56
|
carrier: 'UPS',
|
|
56
57
|
sku: 'SKU-A',
|
|
57
58
|
protocol: 'wl1',
|
|
59
|
+
bookingLink: 'https://booking.example.com/a-123',
|
|
58
60
|
})
|
|
59
61
|
createdOrderId = order.id
|
|
60
62
|
|
|
@@ -70,6 +72,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
70
72
|
&& e.fields?.pharmacy_sku === 'SKU-A'
|
|
71
73
|
&& e.fields?.pharmacy_externalId === 'EXT-A-123'
|
|
72
74
|
&& e.fields?.pharmacy_protocol === 'wl1'
|
|
75
|
+
&& e.fields?.pharmacy_bookingLink === 'https://booking.example.com/a-123'
|
|
73
76
|
&& typeof e.fields?.pharmacy_id === 'string'
|
|
74
77
|
&& (e.fields?.pharmacy_id as string).length > 0
|
|
75
78
|
&& (e.fields?.pharmacy_id as string) === order.id
|
|
@@ -86,6 +89,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
86
89
|
pharmacy_externalId: beforeNonMatch.fields?.pharmacy_externalId,
|
|
87
90
|
pharmacy_id: beforeNonMatch.fields?.pharmacy_id,
|
|
88
91
|
pharmacy_protocol: beforeNonMatch.fields?.pharmacy_protocol,
|
|
92
|
+
pharmacy_bookingLink: beforeNonMatch.fields?.pharmacy_bookingLink,
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
const nonMatching = await sdk.api.enduser_orders.createOne({
|
|
@@ -114,6 +118,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
114
118
|
&& e.fields?.pharmacy_externalId === snapshot.pharmacy_externalId
|
|
115
119
|
&& e.fields?.pharmacy_id === snapshot.pharmacy_id
|
|
116
120
|
&& e.fields?.pharmacy_protocol === snapshot.pharmacy_protocol
|
|
121
|
+
&& e.fields?.pharmacy_bookingLink === snapshot.pharmacy_bookingLink
|
|
117
122
|
)}
|
|
118
123
|
)
|
|
119
124
|
} finally {
|
|
@@ -130,6 +135,8 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
130
135
|
|
|
131
136
|
const webhookUrl = `${host}/v1/webhooks/beluga`
|
|
132
137
|
const externalOrderId = `EXT-B-${Date.now()}`
|
|
138
|
+
const noLinkOrderId = `EXT-B-NOLINK-${Date.now()}`
|
|
139
|
+
const bookingLink = 'https://booking.example.com/beluga-results'
|
|
133
140
|
|
|
134
141
|
const enduser = await sdk.api.endusers.createOne({})
|
|
135
142
|
const form = await sdk.api.forms.createOne({ title: 'Order Templates Beluga Form' })
|
|
@@ -139,6 +146,16 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
139
146
|
formTitle: form.title,
|
|
140
147
|
})
|
|
141
148
|
|
|
149
|
+
// The no-bookingLink case needs its OWN enduser: a fired trigger is throttled per
|
|
150
|
+
// (trigger, enduser) for a minute, so a second order for the same enduser would be
|
|
151
|
+
// silently skipped and the assertion would pass vacuously.
|
|
152
|
+
const noLinkEnduser = await sdk.api.endusers.createOne({})
|
|
153
|
+
const noLinkFormResponse = await sdk.api.form_responses.createOne({
|
|
154
|
+
formId: form.id,
|
|
155
|
+
enduserId: noLinkEnduser.id,
|
|
156
|
+
formTitle: form.title,
|
|
157
|
+
})
|
|
158
|
+
|
|
142
159
|
const trigger = await sdk.api.automation_triggers.createOne({
|
|
143
160
|
title: TRIGGER_TITLE_BLOCK_B,
|
|
144
161
|
status: 'Active',
|
|
@@ -153,6 +170,13 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
153
170
|
action: buildSetFieldsAction('pharmacy'),
|
|
154
171
|
})
|
|
155
172
|
|
|
173
|
+
const resultsTrigger = await sdk.api.automation_triggers.createOne({
|
|
174
|
+
title: `${TRIGGER_TITLE_BLOCK_B} (Results Available)`,
|
|
175
|
+
status: 'Active',
|
|
176
|
+
event: { type: 'Order Status Equals', info: { source: 'Beluga', status: 'Results Available' } },
|
|
177
|
+
action: buildSetFieldsAction('pharmacy'),
|
|
178
|
+
})
|
|
179
|
+
|
|
156
180
|
try {
|
|
157
181
|
// Step 1: PHARMACY_ORDER_SHIPPED
|
|
158
182
|
const shippedRes = await fetch(webhookUrl, {
|
|
@@ -179,9 +203,10 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
179
203
|
&& e.fields?.pharmacy_externalId === externalOrderId
|
|
180
204
|
&& typeof e.fields?.pharmacy_id === 'string'
|
|
181
205
|
&& (e.fields?.pharmacy_id as string).length > 0
|
|
182
|
-
// Webhook does not set sku/protocol -> default branch in helper -> ''
|
|
206
|
+
// Webhook does not set sku/protocol/bookingLink -> default branch in helper -> ''
|
|
183
207
|
&& e.fields?.pharmacy_sku === ''
|
|
184
208
|
&& e.fields?.pharmacy_protocol === ''
|
|
209
|
+
&& e.fields?.pharmacy_bookingLink === ''
|
|
185
210
|
)}
|
|
186
211
|
)
|
|
187
212
|
|
|
@@ -207,24 +232,84 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
|
|
|
207
232
|
&& e.fields?.pharmacy_externalId === externalOrderId
|
|
208
233
|
)}
|
|
209
234
|
)
|
|
235
|
+
|
|
236
|
+
// Step 3: LAB_ORDER_RESULTS with a bookingLink -> status 'Results Available'
|
|
237
|
+
const resultsRes = await fetch(webhookUrl, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
masterId: `tellescope_${formResponse.id}`,
|
|
242
|
+
event: 'LAB_ORDER_RESULTS',
|
|
243
|
+
orderId: externalOrderId,
|
|
244
|
+
bookingLink,
|
|
245
|
+
}),
|
|
246
|
+
})
|
|
247
|
+
assert(resultsRes.status === 200, `Beluga webhook (results) expected 200, got ${resultsRes.status}`)
|
|
248
|
+
|
|
249
|
+
await wait(undefined, 250)
|
|
250
|
+
|
|
251
|
+
await async_test(
|
|
252
|
+
"Block B: LAB_ORDER_RESULTS resolves {{order.bookingLink}} into an enduser field",
|
|
253
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
254
|
+
{ onResult: e => !!(
|
|
255
|
+
e.fields?.pharmacy_status === 'Results Available'
|
|
256
|
+
&& e.fields?.pharmacy_bookingLink === bookingLink
|
|
257
|
+
&& e.fields?.pharmacy_externalId === externalOrderId
|
|
258
|
+
)}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
// Step 4: LAB_ORDER_RESULTS for a different enduser/order with NO bookingLink -> resolves to ''
|
|
262
|
+
const noLinkRes = await fetch(webhookUrl, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
masterId: `tellescope_${noLinkFormResponse.id}`,
|
|
267
|
+
event: 'LAB_ORDER_RESULTS',
|
|
268
|
+
orderId: noLinkOrderId,
|
|
269
|
+
}),
|
|
270
|
+
})
|
|
271
|
+
assert(noLinkRes.status === 200, `Beluga webhook (results, no link) expected 200, got ${noLinkRes.status}`)
|
|
272
|
+
|
|
273
|
+
await wait(undefined, 250)
|
|
274
|
+
|
|
275
|
+
await async_test(
|
|
276
|
+
"Block B: LAB_ORDER_RESULTS without a bookingLink resolves to ''",
|
|
277
|
+
() => sdk.api.endusers.getOne(noLinkEnduser.id),
|
|
278
|
+
{ onResult: e => !!(
|
|
279
|
+
e.fields?.pharmacy_status === 'Results Available' // proves the trigger did fire
|
|
280
|
+
&& e.fields?.pharmacy_externalId === noLinkOrderId
|
|
281
|
+
&& e.fields?.pharmacy_bookingLink === ''
|
|
282
|
+
)}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await async_test(
|
|
286
|
+
"Block B: the other enduser's bookingLink is untouched by the empty-link order",
|
|
287
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
288
|
+
{ onResult: e => e.fields?.pharmacy_bookingLink === bookingLink }
|
|
289
|
+
)
|
|
210
290
|
} finally {
|
|
211
291
|
// Clean up the order created by the webhook
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
292
|
+
for (const externalId of [externalOrderId, noLinkOrderId]) {
|
|
293
|
+
try {
|
|
294
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
295
|
+
filter: { source: 'Beluga', externalId } as any,
|
|
296
|
+
})
|
|
297
|
+
for (const o of orders) {
|
|
298
|
+
await sdk.api.enduser_orders.deleteOne(o.id).catch(console.error)
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(err)
|
|
218
302
|
}
|
|
219
|
-
} catch (err) {
|
|
220
|
-
console.error(err)
|
|
221
303
|
}
|
|
222
304
|
|
|
223
305
|
await sdk.api.automation_triggers.deleteOne(trigger.id).catch(console.error)
|
|
224
306
|
await sdk.api.automation_triggers.deleteOne(deliveredTrigger.id).catch(console.error)
|
|
307
|
+
await sdk.api.automation_triggers.deleteOne(resultsTrigger.id).catch(console.error)
|
|
225
308
|
await sdk.api.form_responses.deleteOne(formResponse.id).catch(console.error)
|
|
309
|
+
await sdk.api.form_responses.deleteOne(noLinkFormResponse.id).catch(console.error)
|
|
226
310
|
await sdk.api.forms.deleteOne(form.id).catch(console.error)
|
|
227
311
|
await sdk.api.endusers.deleteOne(enduser.id).catch(console.error)
|
|
312
|
+
await sdk.api.endusers.deleteOne(noLinkEnduser.id).catch(console.error)
|
|
228
313
|
}
|
|
229
314
|
}
|
|
230
315
|
|