diff --git a/Gemfile.lock b/Gemfile.lock index c5814ba5ef7..3902b88488e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -461,7 +461,7 @@ GEM net-ssh (6.1.0) newrelic_rpm (9.7.0) nio4r (2.7.4) - nokogiri (1.18.2) + nokogiri (1.18.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) numbers_and_words (0.11.12) diff --git a/app/components/captcha_submit_button_component.html.erb b/app/components/captcha_submit_button_component.html.erb index 88e7033c4ac..a46db02a178 100644 --- a/app/components/captcha_submit_button_component.html.erb +++ b/app/components/captcha_submit_button_component.html.erb @@ -1,3 +1,9 @@ +<% content_for :early_head do %> + <% if recaptcha_script_src.present? %> + <%= content_tag(:script, '', src: recaptcha_script_src, async: true) %> + <% end %> +<% end %> + <%= content_tag( :'lg-captcha-submit-button', **tag_options, @@ -34,7 +40,5 @@ wide: true, **button_options, ).with_content(content) %> - <% if recaptcha_script_src.present? %> - <%= content_tag(:script, '', src: recaptcha_script_src, async: true) %> - <% end %> + <% end %> diff --git a/app/components/tab_navigation_component.scss b/app/components/tab_navigation_component.scss index 2570ce80ccf..7b06503cf67 100644 --- a/app/components/tab_navigation_component.scss +++ b/app/components/tab_navigation_component.scss @@ -16,6 +16,7 @@ @include u-padding(1.5); border-radius: 1.375rem; width: 100%; + text-align: center; } .usa-button--unstyled { diff --git a/app/controllers/concerns/fraud_review_concern.rb b/app/controllers/concerns/fraud_review_concern.rb index 53aeb101a66..3bcb325e425 100644 --- a/app/controllers/concerns/fraud_review_concern.rb +++ b/app/controllers/concerns/fraud_review_concern.rb @@ -32,7 +32,7 @@ def handle_fraud_rejection # bypassing the typical flow of showing the Please Call or Fraud Rejection screens. def in_person_prevent_fraud_redirection? IdentityConfig.store.in_person_proofing_enforce_tmx && - current_user.ipp_enrollment_status_not_passed? && + current_user.ipp_enrollment_status_not_passed_or_in_fraud_review? && (fraud_review_pending? || fraud_rejection?) end diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index 0c71c24c7f3..18f319bfa17 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -34,6 +34,7 @@ def error_hash(message) message: message || I18n.t('doc_auth.errors.general.network_error'), socure: stored_result&.errors&.dig(:socure), pii_validation: stored_result&.errors&.dig(:pii_validation), + unaccepted_id_type: stored_result&.errors&.dig(:unaccepted_id_type), } end diff --git a/app/controllers/concerns/idv/socure_errors_concern.rb b/app/controllers/concerns/idv/socure_errors_concern.rb index 76d2b8eeb3f..a47c6a9498b 100644 --- a/app/controllers/concerns/idv/socure_errors_concern.rb +++ b/app/controllers/concerns/idv/socure_errors_concern.rb @@ -12,7 +12,9 @@ def remaining_attempts end def error_code_for(result) - if result.errors[:socure] + if result.errors[:unaccepted_id_type] + :unaccepted_id_type + elsif result.errors[:socure] result.errors.dig(:socure, :reason_codes).first elsif result.errors[:network] :network diff --git a/app/controllers/concerns/mfa_deletion_concern.rb b/app/controllers/concerns/mfa_deletion_concern.rb index 0b533882da1..0f4c647aa2e 100644 --- a/app/controllers/concerns/mfa_deletion_concern.rb +++ b/app/controllers/concerns/mfa_deletion_concern.rb @@ -4,7 +4,7 @@ module MfaDeletionConcern include RememberDeviceConcern def handle_successful_mfa_deletion(event_type:) - create_user_event(event_type) if event_type + create_user_event(event_type) revoke_remember_device(current_user) event = PushNotification::RecoveryInformationChangedEvent.new(user: current_user) PushNotification::HttpPush.deliver(event) diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index 150689e5648..996a536906f 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -36,9 +36,8 @@ def final_submission_passed? return false if doc_session_uuid.blank? document_capture_session = DocumentCaptureSession.find_by(uuid: doc_session_uuid) - return false if document_capture_session.nil? - document_capture_session.last_doc_auth_result == 'Passed' + !!document_capture_session&.load_result&.success? end def confirm_not_rate_limited_for_phone_and_letter_address_verification diff --git a/app/controllers/concerns/verify_profile_concern.rb b/app/controllers/concerns/verify_profile_concern.rb index 84bfb595dad..7233f823d00 100644 --- a/app/controllers/concerns/verify_profile_concern.rb +++ b/app/controllers/concerns/verify_profile_concern.rb @@ -27,7 +27,7 @@ def pending_profile_policy # bypassing the typical flow of showing the Please Call or Fraud Rejection screens. def user_failed_ipp_with_fraud_review_pending? IdentityConfig.store.in_person_proofing_enforce_tmx && - current_user.ipp_enrollment_status_not_passed? && + current_user.ipp_enrollment_status_not_passed_or_in_fraud_review? && current_user.fraud_review_pending? end end diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index d3311aceac7..6ff0e519f17 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -11,14 +11,18 @@ class DocumentCaptureController < ApplicationController include SocureErrorsConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } + before_action :check_valid_document_capture_session before_action :validate_step_not_completed, only: [:show] - before_action :check_valid_document_capture_session, except: [:update] before_action -> do redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, in_hybrid_mobile: true) end, only: :show before_action :fetch_test_verification_data, only: [:update] def show + if rate_limiter.limited? + redirect_to idv_hybrid_mobile_capture_complete_url + end + session[:socure_docv_wait_polling_started_at] = nil Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]) @@ -74,7 +78,7 @@ def update **result.to_h.merge(analytics_arguments), ) - if result.success? + if result.success? || rate_limiter.limited? redirect_to idv_hybrid_mobile_capture_complete_url else redirect_to idv_hybrid_mobile_socure_document_capture_errors_url @@ -82,7 +86,11 @@ def update end def errors - @presenter = socure_errors_presenter(handle_stored_result) + result = handle_stored_result( + user: document_capture_user, + store_in_session: false, + ) + @presenter = socure_errors_presenter(result) end private @@ -103,7 +111,8 @@ def socure_errors_presenter(result) end def wait_for_result? - return false if stored_result.present? + document_capture_session.reload unless document_capture_session.result_id + return false if document_capture_session.load_result.present? # If the stored_result is nil, the job fetching the results has not completed. analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) @@ -141,6 +150,13 @@ def analytics_arguments pii_like_keypaths: [[:pii]], } end + + def rate_limiter + @rate_limiter ||= RateLimiter.new( + user: document_capture_user, + rate_limit_type: :idv_doc_auth, + ) + end end end end diff --git a/app/controllers/idv/hybrid_mobile/socure/errors_controller.rb b/app/controllers/idv/hybrid_mobile/socure/errors_controller.rb index 37b1d697917..821c94f558b 100644 --- a/app/controllers/idv/hybrid_mobile/socure/errors_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/errors_controller.rb @@ -10,10 +10,16 @@ class ErrorsController < ApplicationController include StepIndicatorConcern include SocureErrorsConcern + before_action :check_valid_document_capture_session + def show error_code = error_params[:error_code] if error_code.nil? - error_code = error_code_for(handle_stored_result) + result = handle_stored_result( + user: document_capture_session.user, + store_in_session: false, + ) + error_code = error_code_for(result) end track_event(error_code: error_code) @presenter = socure_errors_presenter(error_code) @@ -39,7 +45,7 @@ def error_params end def rate_limiter - RateLimiter.new(user: document_capture_session&.user, rate_limit_type: :idv_doc_auth) + RateLimiter.new(user: document_capture_session.user, rate_limit_type: :idv_doc_auth) end def remaining_submit_attempts @@ -67,7 +73,7 @@ def socure_errors_presenter(error_code) end def service_provider - @service_provider ||= ServiceProvider.find_by(issuer: document_capture_session&.issuer) + @service_provider ||= ServiceProvider.find_by(issuer: document_capture_session.issuer) end end end diff --git a/app/controllers/idv/please_call_controller.rb b/app/controllers/idv/please_call_controller.rb index 00c027b2cb1..dc34c6f84d5 100644 --- a/app/controllers/idv/please_call_controller.rb +++ b/app/controllers/idv/please_call_controller.rb @@ -15,12 +15,12 @@ def show analytics.idv_please_call_visited pending_at = current_user.fraud_review_pending_profile.fraud_review_pending_at @call_by_date = pending_at + FRAUD_REVIEW_CONTACT_WITHIN_DAYS - @in_person = ipp_enabled_and_enrollment_passed? + @in_person = ipp_enabled_and_enrollment_passed_or_in_fraud_review? end - def ipp_enabled_and_enrollment_passed? + def ipp_enabled_and_enrollment_passed_or_in_fraud_review? return unless in_person_tmx_enabled? - in_person_proofing_enabled? && ipp_enrollment_passed? + in_person_proofing_enabled? && (ipp_enrollment_passed? || ipp_enrollment_in_fraud_review?) end private @@ -43,5 +43,9 @@ def in_person_tmx_enabled? def ipp_enrollment_passed? current_user&.in_person_enrollment_status == 'passed' end + + def ipp_enrollment_in_fraud_review? + current_user&.in_person_enrollment_status == 'in_fraud_review' + end end end diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index 030a1556ae2..08afa045b53 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -109,7 +109,8 @@ def self.step_info private def wait_for_result? - return false if stored_result.present? + document_capture_session.reload unless document_capture_session.result_id + return false if document_capture_session.load_result.present? # If the stored_result is nil, the job fetching the results has not completed. analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) diff --git a/app/controllers/idv/socure/errors_controller.rb b/app/controllers/idv/socure/errors_controller.rb index 7d3259896e4..0f57a13479f 100644 --- a/app/controllers/idv/socure/errors_controller.rb +++ b/app/controllers/idv/socure/errors_controller.rb @@ -69,7 +69,9 @@ def socure_errors_presenter(error_code) end def error_code_for(result) - if result.errors[:socure] + if result.errors[:unaccepted_id_type] + :unaccepted_id_type + elsif result.errors[:socure] result.errors.dig(:socure, :reason_codes).first elsif result.errors[:network] :network diff --git a/app/controllers/idv/welcome_controller.rb b/app/controllers/idv/welcome_controller.rb index 8284e069c2a..75907f3e93a 100644 --- a/app/controllers/idv/welcome_controller.rb +++ b/app/controllers/idv/welcome_controller.rb @@ -62,8 +62,9 @@ def create_document_capture_session def cancel_previous_in_person_enrollments return unless IdentityConfig.store.in_person_proofing_enabled - UspsInPersonProofing::EnrollmentHelper - .cancel_establishing_and_pending_enrollments(current_user) + UspsInPersonProofing::EnrollmentHelper.cancel_establishing_and_in_progress_enrollments( + current_user, + ) end end end diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb index 4d9ece3644a..bcea97b60ef 100644 --- a/app/controllers/users/backup_code_setup_controller.rb +++ b/app/controllers/users/backup_code_setup_controller.rb @@ -59,7 +59,7 @@ def refreshed def delete current_user.backup_code_configurations.destroy_all - handle_successful_mfa_deletion(event_type: nil) + handle_successful_mfa_deletion(event_type: :backup_codes_removed) flash[:success] = t('notices.backup_codes_deleted') if in_multi_mfa_selection_flow? redirect_to authentication_methods_setup_path diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index 479ec1bfe4b..64066b0f4ec 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -24,7 +24,6 @@ def submit(params) success: valid?, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/frontend_error_form.rb b/app/forms/frontend_error_form.rb index f9ed8046632..1bff030b0c6 100644 --- a/app/forms/frontend_error_form.rb +++ b/app/forms/frontend_error_form.rb @@ -13,7 +13,7 @@ def submit(filename:, error_id:) @filename = filename @error_id = error_id - FormResponse.new(success: valid?, errors:, serialize_error_details_only: true) + FormResponse.new(success: valid?, errors:) end private diff --git a/app/forms/otp_verification_form.rb b/app/forms/otp_verification_form.rb index c4cd27b545d..30f60750abc 100644 --- a/app/forms/otp_verification_form.rb +++ b/app/forms/otp_verification_form.rb @@ -25,7 +25,6 @@ def submit success: success, errors: errors, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/recaptcha_form.rb b/app/forms/recaptcha_form.rb index 2ce7c2b1d16..770b06289bd 100644 --- a/app/forms/recaptcha_form.rb +++ b/app/forms/recaptcha_form.rb @@ -54,11 +54,11 @@ def submit(recaptcha_token) @recaptcha_result = recaptcha_result if recaptcha_token.present? && !exempt? log_analytics(result: @recaptcha_result) if @recaptcha_result - response = FormResponse.new(success: valid?, errors:, serialize_error_details_only: true) + response = FormResponse.new(success: valid?, errors:) [response, @recaptcha_result&.assessment_id] rescue Faraday::Error => error log_analytics(error:) - response = FormResponse.new(success: true, serialize_error_details_only: true) + response = FormResponse.new(success: true) [response, nil] end diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index 3a0d2b4cc03..14feb592407 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -70,12 +70,12 @@ def mark_profile_as_password_reset def password_reset_profile FeatureManagement.pending_in_person_password_reset_enabled? ? - find_pending_in_person_or_active_profile : + find_in_progress_in_person_or_active_profile : active_profile end - def find_pending_in_person_or_active_profile - user.pending_in_person_enrollment&.profile || active_profile + def find_in_progress_in_person_or_active_profile + user.current_in_progress_in_person_enrollment_profile || active_profile end # It is possible for an account that is resetting their password to be "invalid". @@ -104,7 +104,9 @@ def extra_analytics_attributes def pending_profile_invalidated? if FeatureManagement.pending_in_person_password_reset_enabled? - pending_profile.present? && !pending_profile.in_person_verification_pending? + pending_profile.present? && + !pending_profile.in_person_verification_pending? && + !pending_profile.fraud_deactivation_reason? else pending_profile.present? end diff --git a/app/forms/select_email_form.rb b/app/forms/select_email_form.rb index e4c86499f0b..bbd89bff29e 100644 --- a/app/forms/select_email_form.rb +++ b/app/forms/select_email_form.rb @@ -26,7 +26,6 @@ def submit(params) success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/sign_in_recaptcha_form.rb b/app/forms/sign_in_recaptcha_form.rb index 4f8b2923c59..9bbdb422ef8 100644 --- a/app/forms/sign_in_recaptcha_form.rb +++ b/app/forms/sign_in_recaptcha_form.rb @@ -28,7 +28,7 @@ def submit(recaptcha_token:) @recaptcha_token = recaptcha_token success = valid? - FormResponse.new(success:, errors:, serialize_error_details_only: true) + FormResponse.new(success:, errors:) end def exempt? diff --git a/app/forms/two_factor_authentication/auth_app_delete_form.rb b/app/forms/two_factor_authentication/auth_app_delete_form.rb index ea7274d2bd3..70a5f8a0255 100644 --- a/app/forms/two_factor_authentication/auth_app_delete_form.rb +++ b/app/forms/two_factor_authentication/auth_app_delete_form.rb @@ -24,7 +24,6 @@ def submit success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/two_factor_authentication/auth_app_update_form.rb b/app/forms/two_factor_authentication/auth_app_update_form.rb index cbecad4c8e5..f950ddc7cb4 100644 --- a/app/forms/two_factor_authentication/auth_app_update_form.rb +++ b/app/forms/two_factor_authentication/auth_app_update_form.rb @@ -30,7 +30,6 @@ def submit(name:) success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/two_factor_authentication/piv_cac_delete_form.rb b/app/forms/two_factor_authentication/piv_cac_delete_form.rb index 4570546fcfb..bf9a6e08520 100644 --- a/app/forms/two_factor_authentication/piv_cac_delete_form.rb +++ b/app/forms/two_factor_authentication/piv_cac_delete_form.rb @@ -24,7 +24,6 @@ def submit success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/two_factor_authentication/piv_cac_update_form.rb b/app/forms/two_factor_authentication/piv_cac_update_form.rb index bbce096d865..e9d3c16f6b1 100644 --- a/app/forms/two_factor_authentication/piv_cac_update_form.rb +++ b/app/forms/two_factor_authentication/piv_cac_update_form.rb @@ -30,7 +30,6 @@ def submit(name:) success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/two_factor_authentication/webauthn_delete_form.rb b/app/forms/two_factor_authentication/webauthn_delete_form.rb index 66b90c133cf..0a0732bdc30 100644 --- a/app/forms/two_factor_authentication/webauthn_delete_form.rb +++ b/app/forms/two_factor_authentication/webauthn_delete_form.rb @@ -27,7 +27,6 @@ def submit success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/two_factor_authentication/webauthn_update_form.rb b/app/forms/two_factor_authentication/webauthn_update_form.rb index 6d104afe8ab..218d4e9c9dd 100644 --- a/app/forms/two_factor_authentication/webauthn_update_form.rb +++ b/app/forms/two_factor_authentication/webauthn_update_form.rb @@ -30,7 +30,6 @@ def submit(name:) success:, errors:, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/forms/verify_password_form.rb b/app/forms/verify_password_form.rb index 0daa72f401e..56a49260bcc 100644 --- a/app/forms/verify_password_form.rb +++ b/app/forms/verify_password_form.rb @@ -19,7 +19,7 @@ def submit @personal_key = reencrypt_pii if success - FormResponse.new(success:, errors:, serialize_error_details_only: true) + FormResponse.new(success:, errors:) end private diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb index 9a04e4c3219..aca25d7a8ed 100644 --- a/app/forms/webauthn_verification_form.rb +++ b/app/forms/webauthn_verification_form.rb @@ -56,7 +56,6 @@ def submit success: success, errors: errors, extra: extra_analytics_attributes, - serialize_error_details_only: true, ) end diff --git a/app/jobs/fraud_rejection_daily_job.rb b/app/jobs/fraud_rejection_daily_job.rb index b99aa9e0258..4169379113c 100644 --- a/app/jobs/fraud_rejection_daily_job.rb +++ b/app/jobs/fraud_rejection_daily_job.rb @@ -5,6 +5,7 @@ class FraudRejectionDailyJob < ApplicationJob def perform(_date) profiles_eligible_for_fraud_rejection.find_each do |profile| + profile.in_person_enrollment&.failed! profile.reject_for_fraud(notify_user: false) analytics(user: profile.user).automatic_fraud_rejection( fraud_rejection_at: profile.fraud_rejection_at, diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 4eb13e9f343..fe0ade9b212 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -34,6 +34,7 @@ def perform(_now) enrollments_cancelled: 0, enrollments_in_progress: 0, enrollments_passed: 0, + enrollments_in_fraud_review: 0, enrollments_skipped: 0, } @@ -142,8 +143,7 @@ def check_enrollment(enrollment) def cancel_enrollment(enrollment) enrollment_outcomes[:enrollments_cancelled] += 1 - enrollment.cancelled! - enrollment.profile.deactivate_due_to_in_person_verification_cancelled + enrollment.cancel end def skip_enrollment(enrollment, profile_deactivation_reason) @@ -484,7 +484,7 @@ def handle_successful_status_update(enrollment, response) def handle_passed_with_fraud_review_pending(enrollment, response) proofed_at = parse_usps_timestamp(response['transactionEndDateTime']) - enrollment_outcomes[:enrollments_passed] += 1 + enrollment_outcomes[:enrollments_in_fraud_review] += 1 log_enrollment_updated_analytics( enrollment: enrollment, enrollment_passed: true, @@ -493,7 +493,7 @@ def handle_passed_with_fraud_review_pending(enrollment, response) reason: 'Passed with fraud pending', ) enrollment.update( - status: :passed, + status: :in_fraud_review, proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) @@ -641,7 +641,7 @@ def send_enrollment_status_sms_notification(enrollment:) end def notification_delivery_params(enrollment) - return {} unless enrollment.passed? || enrollment.failed? + return {} unless enrollment.passed? || enrollment.failed? || enrollment.in_fraud_review? wait_until = enrollment.status_check_completed_at + ( IdentityConfig.store.in_person_results_delay_in_hours || DEFAULT_EMAIL_DELAY_IN_HOURS diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index cd26425d7e9..68eee53b1f9 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -24,6 +24,10 @@ def perform(document_capture_session_uuid:, async: true, docv_transaction_token_ vendor_request_time_in_ms: timer.results['vendor_request'], ) + # for ipp enrollment to track if user attempted doc auth + last_doc_auth_result = docv_result_response.extra_attributes.dig(:decision, :value) + document_capture_session.update!(last_doc_auth_result:) if last_doc_auth_result + if docv_result_response.success? doc_pii_response = Idv::DocPiiForm.new(pii: docv_result_response.pii_from_doc.to_h).submit log_pii_validation(doc_pii_response:) diff --git a/app/models/event.rb b/app/models/event.rb index ed1ea10ce41..e2d5e60664b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -31,6 +31,7 @@ class Event < ApplicationRecord sign_in_notification_timeframe_expired: 24, webauthn_platform_added: 25, webauthn_platform_removed: 26, + backup_codes_removed: 27, } validates :event_type, presence: true diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index a89f5fa2d98..ea81229c386 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -13,12 +13,15 @@ class InPersonEnrollment < ApplicationRecord has_one :notification_phone_configuration, dependent: :destroy, inverse_of: :in_person_enrollment + IN_PROGRESS_ENROLLMENT_STATUSES = %w[pending in_fraud_review].to_set.freeze + STATUS_ESTABLISHING = 'establishing' STATUS_PENDING = 'pending' STATUS_PASSED = 'passed' STATUS_FAILED = 'failed' STATUS_EXPIRED = 'expired' STATUS_CANCELLED = 'cancelled' + STATUS_IN_FRAUD_REVIEW = 'in_fraud_review' enum :status, { STATUS_ESTABLISHING.to_sym => 0, @@ -27,6 +30,7 @@ class InPersonEnrollment < ApplicationRecord STATUS_FAILED.to_sym => 3, STATUS_EXPIRED.to_sym => 4, STATUS_CANCELLED.to_sym => 5, + STATUS_IN_FRAUD_REVIEW.to_sym => 6, } validate :profile_belongs_to_user diff --git a/app/models/user.rb b/app/models/user.rb index e5878e72b1c..98289aa8931 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -222,9 +222,14 @@ def in_person_enrollment_status pending_profile&.in_person_enrollment&.status end - def ipp_enrollment_status_not_passed? + # Whether the user's in person enrollment status is not passed or in_fraud_review. Enrollments + # used to go to passed status when profiles were marked as in fraud review. Since LG-15216, this + # will no longer be the case. + def ipp_enrollment_status_not_passed_or_in_fraud_review? !in_person_enrollment_status.blank? && - in_person_enrollment_status != 'passed' + [InPersonEnrollment::STATUS_PASSED, InPersonEnrollment::STATUS_IN_FRAUD_REVIEW].exclude?( + in_person_enrollment_status, + ) end def has_in_person_enrollment? @@ -530,11 +535,19 @@ def last_sign_in_email_address email_addresses.confirmed.last_sign_in end + # Find the user's most recent in-progress enrollment profile. + def current_in_progress_in_person_enrollment_profile + in_person_enrollments + .where(status: InPersonEnrollment::IN_PROGRESS_ENROLLMENT_STATUSES) + .order(created_at: :desc) + .first&.profile + end + private def find_password_reset_profile FeatureManagement.pending_in_person_password_reset_enabled? ? - find_pending_in_person_or_active_profile : + find_in_person_in_progress_or_active_profile : find_active_profile end @@ -542,9 +555,8 @@ def find_active_profile profiles.where.not(activated_at: nil).order(activated_at: :desc).first end - def find_pending_in_person_or_active_profile - pending_in_person_enrollment&.profile || - profiles.where.not(activated_at: nil).order(activated_at: :desc).first + def find_in_person_in_progress_or_active_profile + current_in_progress_in_person_enrollment_profile || find_active_profile end def lockout_period diff --git a/app/presenters/socure_error_presenter.rb b/app/presenters/socure_error_presenter.rb index 814ffdb98a9..f6f687a27de 100644 --- a/app/presenters/socure_error_presenter.rb +++ b/app/presenters/socure_error_presenter.rb @@ -144,6 +144,8 @@ def heading_string_for(error_code) t('doc_auth.headers.general.network_error') when :timeout, :url_not_found t('idv.errors.technical_difficulties') + when :unaccepted_id_type + t('doc_auth.headers.unaccepted_id_type') else # i18n-tasks-use t('doc_auth.headers.unreadable_id') # i18n-tasks-use t('doc_auth.headers.unaccepted_id_type') @@ -161,6 +163,8 @@ def error_string_for(error_code) t('doc_auth.errors.general.new_network_error') when :timeout, :url_not_found t('idv.errors.try_again_later') + when :unaccepted_id_type + t('doc_auth.errors.unaccepted_id_type') else if remapped_error(error_code) == 'underage' # special handling because it says 'Login.gov' I18n.t('doc_auth.errors.underage', app_name: APP_NAME) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index dfcd57a6e3b..a5fe9e71550 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -187,7 +187,6 @@ def account_reset_recovery_options_visit # @identity.idp.previous_event_name Account Reset # @param [Boolean] success - # @param [Hash] errors Errors resulting from form validation # @param [Boolean] sms_phone does the user have a phone factor configured? # @param [Boolean] totp does the user have an authentication app as a 2FA option? # @param [Boolean] piv_cac does the user have PIV/CAC as a 2FA option? @@ -197,7 +196,6 @@ def account_reset_recovery_options_visit # An account reset has been requested def account_reset_request( success:, - errors:, sms_phone:, totp:, piv_cac:, @@ -209,7 +207,6 @@ def account_reset_request( track_event( 'Account Reset: request', success:, - errors:, sms_phone:, totp:, piv_cac:, @@ -604,7 +601,6 @@ def email_sent(action:, ses_message_id:, email_address_id:, **extra) end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [Time, nil] event_created_at timestamp for the event # @param [Time, nil] disavowed_device_last_used_at @@ -617,7 +613,6 @@ def email_sent(action:, ses_message_id:, email_address_id:, **extra) # Tracks disavowed event def event_disavowal( success:, - errors:, user_id:, error_details: nil, event_created_at: nil, @@ -632,7 +627,6 @@ def event_disavowal( track_event( 'Event disavowal visited', success:, - errors:, error_details:, event_created_at:, disavowed_device_last_used_at:, @@ -1367,12 +1361,12 @@ def idv_doc_auth_document_capture_polling_wait_visited( # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing def idv_doc_auth_document_capture_submitted( success:, - errors:, step:, analytics_id:, liveness_checking_required:, selfie_check_required:, flow_path:, + errors: nil, opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, redo_document_capture: nil, @@ -3340,6 +3334,7 @@ def idv_in_person_usps_proofing_enrollment_code_email_received( # @param [Integer] enrollments_failed number of enrollments which failed identity proofing # @param [Integer] enrollments_in_progress number of enrollments which did not have any change # @param [Integer] enrollments_passed number of enrollments which passed identity proofing + # @param [Integer] enrollments_in_fraud_review number of enrollments in fraud review # @param [Integer] enrollments_skipped number of enrollments skipped # @param [Integer] enrollments_network_error # @param [Integer] enrollments_cancelled @@ -3354,6 +3349,7 @@ def idv_in_person_usps_proofing_results_job_completed( enrollments_failed:, enrollments_in_progress:, enrollments_passed:, + enrollments_in_fraud_review:, enrollments_skipped:, enrollments_network_error:, enrollments_cancelled:, @@ -3371,6 +3367,7 @@ def idv_in_person_usps_proofing_results_job_completed( enrollments_failed:, enrollments_in_progress:, enrollments_passed:, + enrollments_in_fraud_review:, enrollments_skipped:, enrollments_network_error:, enrollments_cancelled:, @@ -4254,7 +4251,6 @@ def idv_phone_confirmation_otp_rate_limit_sends( end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param ["sms", "voice"] otp_delivery_preference Channel used to send the message # @param [String] country_code Abbreviated 2-letter country code associated with phone number @@ -4276,7 +4272,6 @@ def idv_phone_confirmation_otp_rate_limit_sends( # The user resent an OTP during the IDV phone step def idv_phone_confirmation_otp_resent( success:, - errors:, otp_delivery_preference:, country_code:, area_code:, @@ -4293,7 +4288,6 @@ def idv_phone_confirmation_otp_resent( track_event( 'IdV: phone confirmation otp resent', success:, - errors:, error_details:, otp_delivery_preference:, country_code:, @@ -4310,7 +4304,6 @@ def idv_phone_confirmation_otp_resent( end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param ["sms", "voice"] otp_delivery_preference Channel used to send the message # @param [String] country_code Abbreviated 2-letter country code associated with phone number @@ -4332,7 +4325,6 @@ def idv_phone_confirmation_otp_resent( # The user requested an OTP to confirm their phone during the IDV phone step def idv_phone_confirmation_otp_sent( success:, - errors:, otp_delivery_preference:, country_code:, area_code:, @@ -4349,7 +4341,6 @@ def idv_phone_confirmation_otp_sent( track_event( 'IdV: phone confirmation otp sent', success:, - errors:, error_details:, otp_delivery_preference:, country_code:, @@ -4366,7 +4357,6 @@ def idv_phone_confirmation_otp_sent( end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [Boolean] code_expired if the one-time code expired # @param [Boolean] code_matches @@ -4389,7 +4379,6 @@ def idv_phone_confirmation_otp_sent( # When a user attempts to confirm possession of a new phone number during the IDV process def idv_phone_confirmation_otp_submitted( success:, - errors:, code_expired:, code_matches:, otp_delivery_preference:, @@ -4407,7 +4396,6 @@ def idv_phone_confirmation_otp_submitted( track_event( 'IdV: phone confirmation otp submitted', success:, - errors:, error_details:, code_expired:, code_matches:, @@ -6367,7 +6355,6 @@ def pending_account_reset_visited end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [Integer] emails Number of email addresses the notification was sent to # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that @@ -6375,7 +6362,6 @@ def pending_account_reset_visited # Alert user if a personal key was used to sign in def personal_key_alert_about_sign_in( success:, - errors:, emails:, sms_message_ids:, error_details: nil, @@ -6384,7 +6370,6 @@ def personal_key_alert_about_sign_in( track_event( 'Personal key: Alert user about sign in', success:, - errors:, error_details:, emails:, sms_message_ids:, @@ -6621,7 +6606,6 @@ def profile_personal_key_create end # @param [Boolean] success Whether form validation was successful - # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [Integer] emails Number of email addresses the notification was sent to # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that @@ -6630,7 +6614,6 @@ def profile_personal_key_create # were sent to phone numbers and email addresses for the user def profile_personal_key_create_notifications( success:, - errors:, emails:, sms_message_ids:, error_details: nil, @@ -6639,7 +6622,6 @@ def profile_personal_key_create_notifications( track_event( 'Profile: Created new personal key notifications', success:, - errors:, error_details:, emails:, sms_message_ids:, @@ -7086,10 +7068,10 @@ def sign_in_security_check_failed_visited # Tracks when a user opts into SMS def sms_opt_in_submitted( success:, - errors:, new_user:, has_other_auth_methods:, phone_configuration_id:, + errors: nil, error_details: nil, **extra ) diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb index 58d9690a5a3..94b918b0517 100644 --- a/app/services/doc_auth/response.rb +++ b/app/services/doc_auth/response.rb @@ -96,5 +96,9 @@ def selfie_status # to be implemented by concrete subclass :not_processed end + + def extra_attributes + {} + end end end diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index 2f62a05fce5..5ab5bdca2eb 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -44,7 +44,7 @@ def initialize(http_response:, @pii_from_doc = read_pii super( - success: successful_result?, + success: doc_auth_success?, errors: error_messages, pii_from_doc:, extra: extra_attributes, @@ -62,7 +62,7 @@ def initialize(http_response:, end def doc_auth_success? - success? + id_type_supported? && successful_result? end def selfie_status @@ -80,11 +80,11 @@ def extra_attributes reason_codes: get_data(DATA_PATHS[:reason_codes]), document_type: get_data(DATA_PATHS[:document_type]), state: state, - state_id_type: state_id_type, + state_id_type:, flow_path: nil, liveness_checking_required: @biometric_comparison_required, issue_year: state_id_issued&.year, - doc_auth_success: successful_result?, + doc_auth_success: doc_auth_success?, vendor: 'Socure', # TODO: Replace with Idp::Constants::Vendors::SOCURE address_line2_present: address2.present?, zip_code: zipcode, @@ -100,7 +100,9 @@ def successful_result? end def error_messages - if !successful_result? + if !id_type_supported? + { unaccepted_id_type: true } + elsif !successful_result? { socure: { reason_codes: get_data(DATA_PATHS[:reason_codes]) } } else {} @@ -159,8 +161,11 @@ def state_id_issued end def state_id_type - type = get_data(DATA_PATHS[:id_type]) - type&.gsub(/\W/, '')&.underscore + document_id_type&.gsub(/\W/, '')&.underscore + end + + def document_id_type + get_data(DATA_PATHS[:id_type]) end def dob @@ -180,6 +185,10 @@ def parse_date(date_string) Rails.logger.info(message) nil end + + def id_type_supported? + DocAuth::Response::ID_TYPE_SLUGS.key?(document_id_type) + end end end end diff --git a/app/services/form_response.rb b/app/services/form_response.rb index e03e577b7d0..5e77ca3908d 100644 --- a/app/services/form_response.rb +++ b/app/services/form_response.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true class FormResponse - attr_reader :errors, :extra, :serialize_error_details_only + attr_reader :errors, :extra - alias_method :serialize_error_details_only?, :serialize_error_details_only - - def initialize(success:, errors: {}, extra: {}, serialize_error_details_only: false) + def initialize(success:, errors: nil, extra: {}) @success = success - @errors = errors.is_a?(ActiveModel::Errors) ? errors.messages.to_hash : errors - @error_details = errors.details if errors.is_a?(ActiveModel::Errors) + @errors = errors.is_a?(ActiveModel::Errors) ? errors.messages.to_hash : errors.to_h + @error_details = errors&.details if !errors.is_a?(Hash) @extra = extra - @serialize_error_details_only = serialize_error_details_only end def success? @@ -19,7 +16,7 @@ def success? def to_h hash = { success: success } - hash[:errors] = errors.presence if !defined?(@error_details) && !serialize_error_details_only? + hash[:errors] = errors.presence if !defined?(@error_details) hash[:error_details] = flatten_details(error_details) if error_details.present? hash.merge!(extra) hash diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb index d4ec71cba0d..197cd9a34df 100644 --- a/app/services/usps_in_person_proofing/enrollment_helper.rb +++ b/app/services/usps_in_person_proofing/enrollment_helper.rb @@ -91,13 +91,15 @@ def cancel_stale_establishing_enrollments_for_user(user) .find_each(&:cancelled!) end - # Cancel a user's associated establishing and pending in-person enrollments. + # Cancel a user's associated establishing, pending, and in_fraud_review in-person enrollments. # # @param user [User] The user model - def cancel_establishing_and_pending_enrollments(user) + def cancel_establishing_and_in_progress_enrollments(user) user .in_person_enrollments - .where(status: [:establishing, :pending]) + .where(status: + [InPersonEnrollment::STATUS_ESTABLISHING] + + InPersonEnrollment::IN_PROGRESS_ENROLLMENT_STATUSES.to_a) .find_each(&:cancel) end diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index d02278b9932..45dd4259fed 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -20,6 +20,7 @@ <% end %> <%= preload_link_tag font_path('public-sans/PublicSans-Bold.woff2') %> <%= preload_link_tag font_path('public-sans/PublicSans-Regular.woff2') %> + <%= yield(:early_head) if content_for?(:early_head) %> <%= stylesheet_link_tag 'application', nopush: false %> <%= render_stylesheet_once_tags %> <%= stylesheet_link_tag 'utilities', nopush: false %> diff --git a/app/views/user_mailer/reset_password_instructions.html.erb b/app/views/user_mailer/reset_password_instructions.html.erb index bed12ed7ed7..d87995ed1a4 100644 --- a/app/views/user_mailer/reset_password_instructions.html.erb +++ b/app/views/user_mailer/reset_password_instructions.html.erb @@ -17,7 +17,7 @@ <% end %> -<% if @in_person_verification_pending_profile %> +<% if @in_person_verification_pending_profile && !IdentityConfig.store.feature_pending_in_person_password_reset_enabled %> <%= render 'user_mailer/shared/in_person_warning_banner' %>

<%= @header || message.subject %> diff --git a/config/initializers/new_relic.rb b/config/initializers/new_relic.rb deleted file mode 100644 index e49d8cabc54..00000000000 --- a/config/initializers/new_relic.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -# monkeypatch to prevent new relic from truncating backtraces. -# stack length is not currently configurable in new relic. -# The MAX_BACKTRACE_FRAMES constant is commented out for reference - -module NewRelic - module Agent - class ErrorCollector - # Maximum number of frames in backtraces. May be made configurable - # in the future. - # MAX_BACKTRACE_FRAMES = 50 - def truncate_trace(trace, _keep_frames = nil) - trace - end - end - end -end diff --git a/config/locales/en.yml b/config/locales/en.yml index 56ce2c5d835..289a5f7e4c7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -825,6 +825,7 @@ event_types.authenticated_at_html: Signed in at %{service_provider_link_html} event_types.authenticator_disabled: Authenticator app removed event_types.authenticator_enabled: Authenticator app added event_types.backup_codes_added: Backup codes added +event_types.backup_codes_removed: Backup codes removed event_types.eastern_timestamp: '%{timestamp} (Eastern)' event_types.email_changed: Email address changed event_types.email_deleted: Email address deleted diff --git a/config/locales/es.yml b/config/locales/es.yml index 28e65e5800c..cb4f8ae9506 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -836,6 +836,7 @@ event_types.authenticated_at_html: Sesión iniciada en %{service_provider_link_h event_types.authenticator_disabled: Aplicación de autenticación eliminada event_types.authenticator_enabled: Aplicación de autenticación agregada event_types.backup_codes_added: Códigos de recuperación añadidos +event_types.backup_codes_removed: Códigos de recuperación eliminados event_types.eastern_timestamp: '%{timestamp} (hora del Este)' event_types.email_changed: Dirección de correo electrónico modificada event_types.email_deleted: Dirección de correo electrónico eliminada diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5bbd292e3f0..03d15d67739 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -825,6 +825,7 @@ event_types.authenticated_at_html: Connecté à %{service_provider_link_html} event_types.authenticator_disabled: Appli d’authentification supprimée event_types.authenticator_enabled: Appli d’authentification ajoutée event_types.backup_codes_added: Codes de sauvegarde ajoutés +event_types.backup_codes_removed: Codes de sauvegarde supprimés event_types.eastern_timestamp: '%{timestamp} (heure de l’Est)' event_types.email_changed: Adresse e-mail modifiée event_types.email_deleted: Adresse e-mail supprimée diff --git a/config/locales/zh.yml b/config/locales/zh.yml index b356c72123c..eb1a13cf347 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -836,6 +836,7 @@ event_types.authenticated_at_html: 已在 %{service_provider_link_html}登录 event_types.authenticator_disabled: 身份证实器应用程序已去掉 event_types.authenticator_enabled: 身份证实器应用程序已添加 event_types.backup_codes_added: 备用代码已添加 +event_types.backup_codes_removed: 备份代码已删除 event_types.eastern_timestamp: '%{timestamp}(东部)' event_types.email_changed: 电邮地址已更改 event_types.email_deleted: 电邮地址已删除 diff --git a/config/newrelic.yml b/config/newrelic.yml index 13399ae57ab..aab5bb3032a 100644 --- a/config/newrelic.yml +++ b/config/newrelic.yml @@ -1,6 +1,8 @@ common: &default_settings code_level_metrics: enabled: false + distributed_tracing: + enabled: false application_logging: forwarding: enabled: false diff --git a/lib/action_account.rb b/lib/action_account.rb index 4d32fcea0dd..85befe68316 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -190,6 +190,7 @@ def run(args:, config:) elsif FraudReviewChecker.new(user).fraud_review_eligible? profile = user.fraud_review_pending_profile profile_fraud_review_pending_at = profile.fraud_review_pending_at + profile.in_person_enrollment&.failed! profile.reject_for_fraud(notify_user: true) success = true @@ -281,6 +282,7 @@ def run(args:, config:) elsif FraudReviewChecker.new(user).fraud_review_eligible? profile = user.fraud_review_pending_profile profile_fraud_review_pending_at = profile.fraud_review_pending_at + profile.in_person_enrollment&.passed! profile.activate_after_passing_review success = true diff --git a/package.json b/package.json index 31a5e819393..c50b3038bea 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "eslint-plugin-testing-library": "^6.2.0", "jsdom": "^25.0.1", "json-schema-to-ts": "^3.1.1", - "mocha": "^10.0.0", + "mocha": "^11.1.0", "mq-polyfill": "^1.1.8", "msw": "^2.6.5", "prettier": "^3.1.0", diff --git a/spec/components/captcha_submit_button_component_spec.rb b/spec/components/captcha_submit_button_component_spec.rb index 4689cabdfc2..cbf42452598 100644 --- a/spec/components/captcha_submit_button_component_spec.rb +++ b/spec/components/captcha_submit_button_component_spec.rb @@ -7,9 +7,10 @@ let(:content) { 'Button' } let(:action) { 'action_name' } let(:options) { { form:, action: } } + let(:instance) { CaptchaSubmitButtonComponent.new(**options).with_content(content) } subject(:rendered) do - render_inline CaptchaSubmitButtonComponent.new(**options).with_content(content) + Nokogiri::HTML.fragment(view_context.render(instance)) end it 'renders with action' do @@ -47,8 +48,10 @@ end it 'renders script tag for recaptcha' do + rendered + early_head = Nokogiri::HTML.fragment(view_context.content_for(:early_head)) src = "https://www.google.com/recaptcha/api.js?render=#{recaptcha_site_key}" - expect(rendered).to have_css("script[src='#{src}']", visible: :all) + expect(early_head).to have_css("script[src='#{src}']", visible: :all) end context 'with recaptcha enterprise' do @@ -57,8 +60,10 @@ end it 'renders script tag for recaptcha' do + rendered + early_head = Nokogiri::HTML.fragment(view_context.content_for(:early_head)) src = "https://www.google.com/recaptcha/enterprise.js?render=#{recaptcha_site_key}" - expect(rendered).to have_css("script[src='#{src}']", visible: :all) + expect(early_head).to have_css("script[src='#{src}']", visible: :all) end end end diff --git a/spec/controllers/concerns/idv_step_concern_spec.rb b/spec/controllers/concerns/idv_step_concern_spec.rb index 4ed251ef252..0475a9a67e6 100644 --- a/spec/controllers/concerns/idv_step_concern_spec.rb +++ b/spec/controllers/concerns/idv_step_concern_spec.rb @@ -259,5 +259,28 @@ def show expect(response).to redirect_to idv_verify_by_mail_enter_code_url end end + + context 'with a passed in-person enrollment and a fraudulent profile' do + let(:user) do + profile = create(:profile, :in_person_fraud_review_pending, in_person_enrollment: nil) + create(:in_person_enrollment, :passed, profile:, user: profile.user).user + end + + it 'redirects to please call page' do + get :show + + expect(response).to redirect_to idv_please_call_url + end + end + + context 'with a in_fraud_review in-person enrollment and a fraudulent profile' do + let(:user) { create(:profile, :in_person_fraud_review_pending).user } + + it 'redirects to please call page' do + get :show + + expect(response).to redirect_to idv_please_call_url + end + end end end diff --git a/spec/controllers/concerns/mfa_deletion_concern_spec.rb b/spec/controllers/concerns/mfa_deletion_concern_spec.rb index 45bd0ccf3f5..0193cbffaae 100644 --- a/spec/controllers/concerns/mfa_deletion_concern_spec.rb +++ b/spec/controllers/concerns/mfa_deletion_concern_spec.rb @@ -38,15 +38,5 @@ result end - - context 'with nil event_type argument' do - let(:event_type) { nil } - - it 'does not create user event' do - expect(controller).not_to receive(:create_user_event) - - result - end - end end end diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index faa294e55fd..e85713c0bc0 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -383,6 +383,7 @@ before do stub_sign_in(user) + allow(subject.document_capture_session).to receive(:load_result).and_return(stored_result) end it 'redirects to the capture complete page' do diff --git a/spec/controllers/idv/please_call_controller_spec.rb b/spec/controllers/idv/please_call_controller_spec.rb index 42f11525296..c6eee853731 100644 --- a/spec/controllers/idv/please_call_controller_spec.rb +++ b/spec/controllers/idv/please_call_controller_spec.rb @@ -127,4 +127,66 @@ end end end + + describe '#ipp_enabled_and_enrollment_passed_or_in_fraud_review?' do + context 'when in person tmx is enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).and_return(true) + end + + context 'when ipp is enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + end + + context 'when user has a passed enrollment' do + let!(:enrollment) { create(:in_person_enrollment, :passed, user: user, profile: profile) } + + it 'returns true' do + expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(true) + end + end + + context 'when user has an in_fraud_review enrollment' do + let!(:enrollment) do + create(:in_person_enrollment, :in_fraud_review, user: user, profile: profile) + end + + it 'returns true' do + expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(true) + end + end + + context 'when user has a non passed or in_fraud_review enrollment' do + let!(:enrollment) do + create(:in_person_enrollment, :pending, user: user, profile: profile) + end + + it 'returns false' do + expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(false) + end + end + end + + context 'when ipp is disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false) + end + + it 'returns false' do + expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be(false) + end + end + end + + context 'when in person tmx is disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx).and_return(false) + end + + it 'returns nil' do + expect(subject.ipp_enabled_and_enrollment_passed_or_in_fraud_review?).to be_nil + end + end + end end diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index b7365415ab7..7c7a84cabae 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -24,14 +24,8 @@ ) end - let(:document_capture_session) do - DocumentCaptureSession.create( - user: user, - requested_at: Time.zone.now, - ) - end - before do + document_capture_session = create(:document_capture_session, user:, requested_at: Time.zone.now) allow(IdentityConfig.store).to receive(:socure_docv_enabled) .and_return(socure_docv_enabled) allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint) @@ -75,7 +69,6 @@ describe '#show' do let(:request_class) { DocAuth::Socure::Requests::DocumentRequest } - let(:expected_uuid) { document_capture_session.uuid } let(:expected_language) { :en } let(:response_body) { {} } @@ -87,8 +80,6 @@ stub_sign_in(user) stub_up_to(:hybrid_handoff, idv_session: subject.idv_session) - - subject.idv_session.document_capture_session_uuid = expected_uuid end context 'when we try to use this controller but we should be using the LN/mock version' do @@ -168,7 +159,7 @@ before do allow(request_class).to receive(:new).and_call_original allow(I18n).to receive(:locale).and_return(expected_language) - allow(DocumentCaptureSession).to receive(:find_by).and_return(document_capture_session) + get(:show) end @@ -191,8 +182,8 @@ end it 'sets DocumentCaptureSession socure_docv_capture_app_url value' do - document_capture_session.reload - expect(document_capture_session.socure_docv_capture_app_url).to eq(socure_capture_app_url) + expect(subject.document_capture_session.reload.socure_docv_capture_app_url) + .to eq(socure_capture_app_url) end context 'language is english' do @@ -248,8 +239,7 @@ end it 'puts the docvTransactionToken into the document capture session' do - document_capture_session.reload - expect(document_capture_session.socure_docv_transaction_token) + expect(subject.document_capture_session.reload.socure_docv_transaction_token) .to eq(docv_transaction_token) end end @@ -377,7 +367,8 @@ it 'does not create a DocumentRequest when valid capture app exists' do dcs = create( :document_capture_session, - uuid: user.id, + :socure, + user:, socure_docv_capture_app_url: fake_capture_app_url, ) allow(DocumentCaptureSession).to receive(:find_by).and_return(dcs) @@ -392,6 +383,7 @@ before do stub_sign_in(user) subject.idv_session.flow_path = 'standard' + allow(subject.document_capture_session).to receive(:load_result).and_return(stored_result) get :update end @@ -460,6 +452,8 @@ }, body: SocureDocvFixtures.pass_json, ) + + allow_any_instance_of(DocumentCaptureSession).to receive(:load_result).and_call_original end context 'when a token is provided from the allow list' do @@ -467,7 +461,7 @@ expect { get(:update, params: { docv_token: test_token }) } .not_to have_enqueued_job(SocureDocvResultsJob) # is synchronous - expect(document_capture_session.reload.load_result).not_to be_nil + expect(subject.document_capture_session.reload.load_result).not_to be_nil end end @@ -476,7 +470,7 @@ expect { get(:update, params: { docv_token: 'rando-token' }) } .not_to have_enqueued_job(SocureDocvResultsJob) - expect(document_capture_session.reload.load_result).to be_nil + expect(subject.document_capture_session.reload.load_result).to be_nil end end end diff --git a/spec/controllers/idv/welcome_controller_spec.rb b/spec/controllers/idv/welcome_controller_spec.rb index 3fd0e2a2c0b..e7ac6c6183b 100644 --- a/spec/controllers/idv/welcome_controller_spec.rb +++ b/spec/controllers/idv/welcome_controller_spec.rb @@ -140,16 +140,23 @@ let!(:pending_enrollment) do create(:in_person_enrollment, :pending, user: user, profile: password_reset_profile) end + let(:fraud_password_reset_profile) { create(:profile, :password_reset, user: user) } + let!(:fraud_review_enrollment) do + create( + :in_person_enrollment, :in_fraud_review, user: user, profile: fraud_password_reset_profile + ) + end before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end - it 'cancels all previous establishing and pending enrollments' do + it 'cancels all previous establishing, pending, and in_fraud_review enrollments' do put :update expect(establishing_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED) expect(pending_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED) + expect(fraud_review_enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED) expect(user.establishing_in_person_enrollment).to be_blank expect(user.pending_in_person_enrollment).to be_blank end diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 5b93ceffad8..14afdeaff7b 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -1494,6 +1494,44 @@ end end + context 'user has ipp enrollment with fraud review pending' do + context 'when the enrollment has a status of passed' do + let(:user) do + profile = create(:profile, :fraud_review_pending) + create(:in_person_enrollment, :passed, profile:, user: profile.user).user + end + + it 'redirects to fraud review page if fraud review is pending' do + action + expect(controller).to redirect_to(idv_please_call_url) + end + end + + context 'when the enrollment has a status of in_fraud_review' do + let(:user) do + profile = create(:profile, :fraud_review_pending) + create(:in_person_enrollment, :in_fraud_review, profile:, user: profile.user).user + end + + it 'redirects to fraud review page if fraud review is pending' do + action + expect(controller).to redirect_to(idv_please_call_url) + end + end + + context 'when the enrollment does not have a status of pending or in_fraud_review' do + let(:user) do + profile = create(:profile, :fraud_review_pending, deactivation_reason: :verification_cancelled) + create(:in_person_enrollment, :failed, profile:, user: profile.user).user + end + + it 'redirects the user to verify their account' do + action + expect(controller).to redirect_to(idv_url) + end + end + end + context 'user has two pending reasons' do context 'user has gpo and fraud review pending' do let(:user) do diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index 3678ec43901..5cdd5acc6f7 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -43,6 +43,21 @@ status_updated_at { Time.zone.now } end + trait :in_fraud_review do + enrollment_code { Faker::Number.number(digits: 16) } + enrollment_established_at { Time.zone.now } + status { :in_fraud_review } + status_updated_at { Time.zone.now } + profile do + association( + :profile, + :fraud_review_pending, + user: user, + in_person_enrollment: instance, + ) + end + end + trait :with_service_provider do service_provider { association :service_provider } end diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index c20c42667d2..521dcb383f7 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -53,6 +53,16 @@ end end + trait :in_person_fraud_review_pending do + idv_level { :in_person } + fraud_pending_reason { 'threatmetrix_review' } + fraud_review_pending_at { 15.days.ago } + proofing_components { { threatmetrix_review_status: 'review' } } + in_person_enrollment do + association(:in_person_enrollment, :in_fraud_review, profile: instance, user:) + end + end + trait :fraud_pending_reason do fraud_pending_reason { 'threatmetrix_review' } proofing_components { { threatmetrix_review_status: 'review' } } diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index 98d366af79b..988b98f0733 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -151,9 +151,6 @@ socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - DocumentCaptureSession.find_by(user_id: @user.id).update( - last_doc_auth_result: 'Passed', - ) visit idv_socure_document_capture_update_path expect(page).to have_current_path(idv_ssn_url) @@ -388,6 +385,27 @@ end end end + + context 'not accepted id type' do + it 'displays unaccepdted id type error message' do + body = JSON.parse(SocureDocvFixtures.pass_json) + body['documentVerification']['documentType']['type'] = 'Passport' + + remove_request_stub(@pass_stub) + stub_docv_verification_data( + docv_transaction_token: @docv_transaction_token, + body: body.to_json, + ) + + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_update_path + + expect(page).to have_content(t('doc_auth.headers.unaccepted_id_type')) + expect(page).to have_content(t('doc_auth.errors.unaccepted_id_type')) + end + end end context 'standard mobile flow' do diff --git a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb index b157390c519..54db5b73d7f 100644 --- a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb +++ b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb @@ -708,7 +708,7 @@ # Then the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'in_fraud_review', ) # And the user has a Profile that is deactivated with reason "encryption_error" and @@ -728,7 +728,7 @@ expect(page).to have_current_path(idv_please_call_path) # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'in_fraud_review', ) # And the user has a Profile that is pending fraud review expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -829,7 +829,7 @@ # Then the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'in_fraud_review', ) # And the user has a Profile that is pending fraud review expect(@user.in_person_enrollments.first.profile).to have_attributes( diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb index 866794851cb..ea3a6c93540 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -253,12 +253,10 @@ end before do - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :post_front_image, - response: DocAuth::Response.new( - success: false, - errors: { network: I18n.t('doc_auth.errors.general.network_error') }, - ), + remove_request_stub(@pass_stub) + stub_docv_verification_data_fail_with( + docv_transaction_token: @docv_transaction_token, + errors: ['R827'], ) end @@ -290,7 +288,10 @@ end visit idv_hybrid_mobile_socure_document_capture_update_url + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + visit idv_hybrid_mobile_socure_document_capture_url expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) expect(page).to have_text(t('doc_auth.instructions.switch_back')) end @@ -455,6 +456,55 @@ end end end + + context 'invalid ID type' do + it 'presents as an unaccepted ID type error', js: true do + user = nil + + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') + + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + body = JSON.parse(SocureDocvFixtures.pass_json) + body['documentVerification']['documentType']['type'] = 'Passport' + remove_request_stub(@pass_stub) + stub_docv_verification_data( + docv_transaction_token: @docv_transaction_token, + body: body.to_json, + ) + + click_idv_continue + + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_content(t('doc_auth.headers.unaccepted_id_type')) + expect(page).to have_content(t('doc_auth.errors.unaccepted_id_type')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_link_sent_path) + end + end + end end shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key| diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb index c2cf1f46fcc..58ee0f47c8b 100644 --- a/spec/forms/reset_password_form_spec.rb +++ b/spec/forms/reset_password_form_spec.rb @@ -187,6 +187,34 @@ end end + context 'when the profile is in fraud review for in person verification' do + let!(:user) { create(:user, reset_password_sent_at: Time.zone.now) } + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + let(:profile) { enrollment.profile } + + before do + @result = form.submit(params) + profile.reload + end + + it 'returns a successful response' do + expect(@result.success?).to eq(true) + end + + it 'includes that the profile was not deactivated in the form response' do + expect(@result.extra).to include( + user_id: user.uuid, + profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: 'fraud_check_pending', + ) + end + + it 'updates the profile to have a "password reset" deactivation reason' do + expect(profile.deactivation_reason).to eq('password_reset') + end + end + context 'when the user has an active and a pending in-person verification profile' do let!(:user) { create(:user, reset_password_sent_at: Time.zone.now) } let!(:pending_profile) { create(:profile, :in_person_verification_pending, user: user) } @@ -416,6 +444,34 @@ expect(profile.deactivation_reason).to be_nil end end + + context 'when the profile is in fraud review for in person verification' do + let!(:user) { create(:user, reset_password_sent_at: Time.zone.now) } + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + let(:profile) { enrollment.profile } + + before do + @result = form.submit(params) + profile.reload + end + + it 'returns a successful response' do + expect(@result.success?).to eq(true) + end + + it 'includes that the profile was not deactivated in the form response' do + expect(@result.extra).to include( + user_id: user.uuid, + profile_deactivated: false, + pending_profile_invalidated: true, + pending_profile_pending_reasons: 'fraud_check_pending', + ) + end + + it 'does not update the profile to have a "password reset" deactivation reason' do + expect(profile.deactivation_reason).to be_nil + end + end end context 'when the user does not have a pending profile' do diff --git a/spec/forms/totp_verification_form_spec.rb b/spec/forms/totp_verification_form_spec.rb index 7f087cdd976..de21359b48f 100644 --- a/spec/forms/totp_verification_form_spec.rb +++ b/spec/forms/totp_verification_form_spec.rb @@ -13,7 +13,6 @@ expect(form.submit.to_h).to eq( success: true, - errors: nil, auth_app_configuration_id: cfg.id, multi_factor_auth_method_created_at: cfg.created_at.strftime('%s%L'), ) @@ -30,7 +29,6 @@ expect(form.submit.to_h).to eq( success: false, - errors: nil, auth_app_configuration_id: nil, multi_factor_auth_method_created_at: nil, ) @@ -48,7 +46,6 @@ expect(form.submit.to_h).to eq( success: false, - errors: nil, auth_app_configuration_id: nil, multi_factor_auth_method_created_at: nil, ) diff --git a/spec/jobs/fraud_rejection_daily_job_spec.rb b/spec/jobs/fraud_rejection_daily_job_spec.rb index 4c87e8ef5b8..6e8eae2471e 100644 --- a/spec/jobs/fraud_rejection_daily_job_spec.rb +++ b/spec/jobs/fraud_rejection_daily_job_spec.rb @@ -20,5 +20,26 @@ fraud_rejection_at: rejected_profiles.first.fraud_rejection_at, ) end + + it 'rejects in-person profiles which have been review pending for more than 30 days' do + rejectedable_profile = create(:profile, fraud_review_pending_at: 31.days.ago) + enrollment = create( + :in_person_enrollment, :in_fraud_review, profile: rejectedable_profile, + user: rejectedable_profile.user + ) + create(:profile, fraud_review_pending_at: 20.days.ago) + + rejected_profiles = Profile.where.not(fraud_rejection_at: nil) + + allow(job).to receive(:analytics).with(user: rejectedable_profile.user) + .and_return(job_analytics) + + expect { job.perform(Time.zone.today) }.to change { rejected_profiles.count }.by(1) + expect(job_analytics).to have_logged_event( + 'Fraud: Automatic Fraud Rejection', + fraud_rejection_at: rejected_profiles.first.fraud_rejection_at, + ) + expect(enrollment.reload.status).to eq('failed') + end end end diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index bda87a4b899..b0f323129fb 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -18,6 +18,7 @@ enrollments_cancelled: 0, enrollments_in_progress: 0, enrollments_passed: 0, + enrollments_in_fraud_review: 0, enrollments_skipped: 0, duration_seconds: 0.0, percent_enrollments_errored: 0.0, @@ -1604,9 +1605,9 @@ ) end - it 'passes the enrollment' do + it 'updates the enrollment with "in_fraud_review" status' do expect(enrollment.reload).to have_attributes( - status: 'passed', + status: 'in_fraud_review', proofed_at: usps_enrollment_end_date.getlocal('UTC'), status_check_attempted_at: current_time, status_check_completed_at: current_time, @@ -1645,7 +1646,7 @@ ).with( **default_job_completion_analytics, enrollments_checked: 1, - enrollments_passed: 1, + enrollments_in_fraud_review: 1, ) end end diff --git a/spec/jobs/socure_docv_results_job_spec.rb b/spec/jobs/socure_docv_results_job_spec.rb index 8ad61700fe3..3c6dae1d652 100644 --- a/spec/jobs/socure_docv_results_job_spec.rb +++ b/spec/jobs/socure_docv_results_job_spec.rb @@ -15,6 +15,7 @@ let(:socure_idplus_base_url) { 'https://example.com' } let(:decision_value) { 'accept' } let(:expiration_date) { "#{1.year.from_now.year}-01-01" } + let(:document_type_type) { 'Drivers License' } before do allow(IdentityConfig.store).to receive(:socure_idplus_base_url) @@ -40,7 +41,7 @@ documentVerification: { reasonCodes: %w[I831 R810], documentType: { - type: 'Drivers License', + type: document_type_type, country: 'USA', state: 'NY', }, @@ -85,7 +86,7 @@ zip_code: '10001', doc_auth_success: true, document_type: { - type: 'Drivers License', + type: document_type_type, country: 'USA', state: 'NY', }, @@ -112,6 +113,38 @@ expect(document_capture_session_result.attention_with_barcode).to eq(false) expect(document_capture_session_result.doc_auth_success).to eq(true) expect(document_capture_session_result.selfie_status).to eq(:not_processed) + expect(document_capture_session.last_doc_auth_result).to eq('accept') + end + + context 'Identification Card is submitted' do + let(:document_type_type) { 'Identification Card' } + it 'doc auth succeeds' do + perform + + document_capture_session.reload + document_capture_session_result = document_capture_session.load_result + expect(document_capture_session_result.success).to eq(true) + expect(document_capture_session_result.pii[:first_name]).to eq('Dwayne') + expect(document_capture_session_result.attention_with_barcode).to eq(false) + expect(document_capture_session_result.doc_auth_success).to eq(true) + expect(document_capture_session_result.selfie_status).to eq(:not_processed) + expect(document_capture_session.last_doc_auth_result).to eq('accept') + end + end + + context 'not accepted document type' do + let(:document_type_type) { 'Passport' } + it 'doc auth fails' do + perform + + document_capture_session.reload + document_capture_session_result = document_capture_session.load_result + expect(document_capture_session_result.success).to eq(false) + expect(document_capture_session_result.pii[:first_name]).to eq('Dwayne') + expect(document_capture_session_result.attention_with_barcode).to eq(false) + expect(document_capture_session_result.doc_auth_success).to eq(false) + expect(document_capture_session_result.selfie_status).to eq(:not_processed) + end end context 'Socure returns an error' do @@ -191,6 +224,7 @@ ) document_capture_session.reload expect(document_capture_session.load_result).to be_nil + expect(document_capture_session.last_doc_auth_result).to be_nil end end end diff --git a/spec/lib/action_account_spec.rb b/spec/lib/action_account_spec.rb index 939fcca0c53..b4952415603 100644 --- a/spec/lib/action_account_spec.rb +++ b/spec/lib/action_account_spec.rb @@ -184,6 +184,29 @@ ) end + context 'when the user has a pending review from an IPP enrollment' do + let!(:user) { create(:user) } + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + let!(:profile) { enrollment.profile } + + before do + subtask.run(args:, config:) + enrollment.reload + profile.reload + end + + it 'fails the enrollment and rejects the profile' do + expect(enrollment.status).to eq('failed') + expect(profile).to have_attributes( + { + active: false, + fraud_review_pending_at: nil, + fraud_rejection_at: be_a(Time), + }, + ) + end + end + context 'when profile has initiating_service_provider_issuer' do let(:user) do create( @@ -263,6 +286,32 @@ ) end + context 'when the user has a pending review from an IPP enrollment' do + let!(:user) { create(:user) } + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + let!(:profile) { enrollment.profile } + + before do + subtask.run(args:, config:) + enrollment.reload + profile.reload + end + + it 'passes the enrollment and activates the profile' do + expect(enrollment.status).to eq('passed') + expect(profile).to have_attributes( + { + active: true, + activated_at: be_a(Time), + verified_at: be_a(Time), + fraud_review_pending_at: nil, + fraud_rejection_at: nil, + fraud_pending_reason: nil, + }, + ) + end + end + context 'when profile has initiating_service_provider_issuer' do let(:user) do create( diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 93e5c9d94c4..23f604d8b72 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -185,12 +185,34 @@ expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') end - it 'renders the in person warning banner' do - expect(mail.html_part.body).to have_content( - strip_tags( - t('user_mailer.reset_password_instructions.in_person_warning_description_html'), - ), - ) + context 'when feature_pending_in_person_password_reset_enabled flag is true' do + before do + allow(IdentityConfig.store).to receive(:feature_pending_in_person_password_reset_enabled) + .and_return(true) + end + + it 'does not render the in person warning banner' do + expect(mail.html_part.body).not_to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + end + + context 'when feature_pending_in_person_password_reset_enabled flag is false' do + before do + allow(IdentityConfig.store).to receive(:feature_pending_in_person_password_reset_enabled) + .and_return(false) + end + + it 'renders the in person warning banner' do + expect(mail.html_part.body).to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end end it 'does not render the gpo warning alert' do diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index ae78ceb92a6..4832b35a136 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -11,7 +11,8 @@ describe 'Status' do it 'defines enum correctly' do should define_enum_for(:status) - .with_values([:establishing, :pending, :passed, :failed, :expired, :cancelled]) + .with_values([:establishing, :pending, :passed, :failed, :expired, :cancelled, + :in_fraud_review]) end end @@ -256,6 +257,9 @@ ready_for_status_check: true, ) end + let!(:fraud_review_enrollment) do + create(:in_person_enrollment, :in_fraud_review, ready_for_status_check: true) + end let!(:ready_enrollments) do create_list(:in_person_enrollment, 4, :pending, ready_for_status_check: true) end @@ -264,7 +268,7 @@ end it 'needs_status_check_on_ready_enrollments returns only ready pending enrollments' do - expect(InPersonEnrollment.count).to eq(12) + expect(InPersonEnrollment.count).to eq(13) ready_results = InPersonEnrollment.needs_status_check_on_ready_enrollments(check_interval) expect(ready_results.pluck(:id)).to match_array ready_enrollments.pluck(:id) expect(ready_results.pluck(:id)).not_to match_array needy_enrollments.pluck(:id) @@ -277,6 +281,7 @@ failed_enrollment, expired_enrollment, checked_pending_enrollment, + fraud_review_enrollment, ] (other_enrollments + needy_enrollments).each do |enrollment| @@ -289,7 +294,7 @@ end it 'needs_status_check_on_waiting_enrollments returns only not ready pending enrollments' do - expect(InPersonEnrollment.count).to eq(12) + expect(InPersonEnrollment.count).to eq(13) waiting_results = InPersonEnrollment.needs_status_check_on_waiting_enrollments(check_interval) expect(waiting_results.pluck(:id)).to match_array needy_enrollments.pluck(:id) expect(waiting_results.pluck(:id)).not_to match_array ready_enrollments.pluck(:id) @@ -302,6 +307,7 @@ failed_enrollment, expired_enrollment, checked_pending_enrollment, + fraud_review_enrollment, ] (other_enrollments + ready_enrollments).each do |enrollment| @@ -498,6 +504,9 @@ let(:failed_enrollment_without_notification) do create(:in_person_enrollment, :failed) end + let(:in_fraud_review_enrollment) do + create(:in_person_enrollment, :in_fraud_review) + end it 'returns true when status of passed/failed/expired and with notification configuration' do expect(passed_enrollment.eligible_for_notification?).to eq(true) @@ -509,6 +518,7 @@ expect(expired_enrollment.eligible_for_notification?).to eq(false) expect(passed_enrollment_without_notification.eligible_for_notification?).to eq(false) expect(failed_enrollment_without_notification.eligible_for_notification?).to eq(false) + expect(in_fraud_review_enrollment.eligible_for_notification?).to eq(false) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8925199792d..7417fb9627e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -390,6 +390,45 @@ end end + describe '#ipp_enrollment_status_not_passed_or_in_fraud_review?' do + let(:user) { create(:user, :fully_registered) } + + context 'when the user has an in-person enrollment' do + context 'when the in-person enrollment has a status of passed' do + let!(:profile) { create(:profile, :fraud_review_pending, user:) } + let!(:enrollment) { create(:in_person_enrollment, :passed, user:, profile:) } + + it 'returns false' do + expect(user.ipp_enrollment_status_not_passed_or_in_fraud_review?).to be(false) + end + end + + context 'when the in-person enrollment has a status of in_fraud_review' do + let!(:profile) { create(:profile, :fraud_review_pending, user:) } + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user:, profile:) } + + it 'returns false' do + expect(user.ipp_enrollment_status_not_passed_or_in_fraud_review?).to be(false) + end + end + + context 'when the in-person enrollment does not have a status of passed or in_fraud_review' do + let!(:profile) { create(:profile, :fraud_review_pending, user:) } + let!(:enrollment) { create(:in_person_enrollment, :pending, user:, profile:) } + + it 'returns true' do + expect(user.ipp_enrollment_status_not_passed_or_in_fraud_review?).to be(true) + end + end + end + + context 'when the user does not have an in-person enrollment' do + it 'returns false' do + expect(user.ipp_enrollment_status_not_passed_or_in_fraud_review?).to be(false) + end + end + end + describe '#has_establishing_in_person_enrollment?' do context 'when the user has an establishing in person enrollment' do before do @@ -1662,6 +1701,27 @@ def it_should_not_send_survey end end + context 'with a fraud review in person profile' do + let(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + let(:profile) { enrollment.profile } + + context 'when the profile has a "password_reset deactivation reason"' do + before do + profile.update!(deactivation_reason: 'password_reset') + end + + it 'returns the profile' do + expect(user.password_reset_profile).to eq(profile) + end + end + + context 'when the profile does not have a deactivation reason' do + it 'returns nil' do + expect(user.password_reset_profile).to be_nil + end + end + end + context 'with a pending in person and an active profile' do let(:pending_in_person_enrollment) { create(:in_person_enrollment, :pending, user: user) } let(:pending_profile) { pending_in_person_enrollment.profile } @@ -1890,4 +1950,53 @@ def it_should_not_send_survey expect(user.last_sign_in_email_address).to eq(last_sign_in_email_address) end end + + describe '#current_in_progress_in_person_enrollment_profile' do + let(:user) { create(:user) } + + context 'when the user has a pending in-person enrollment' do + let!(:enrollment) { create(:in_person_enrollment, :pending, user: user) } + + it 'returns the enrollments associated profile' do + expect(user.current_in_progress_in_person_enrollment_profile).to eq(enrollment.profile) + end + end + + context 'when the user has an in_fraud_review in-person enrollment' do + let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + + it 'returns the enrollments associated profile' do + expect(user.current_in_progress_in_person_enrollment_profile).to eq(enrollment.profile) + end + end + + context 'when the user has an in_fraud_review and in_fraud_review in-person enrollment' do + let!(:pending_enrollment) { create(:in_person_enrollment, :pending, user: user) } + let!(:fraud_enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } + + context 'when the pending enrollment was created more recently' do + before do + pending_enrollment.update(created_at: Time.zone.now) + end + + it "returns the pending enrollment's associated profile" do + expect(user.current_in_progress_in_person_enrollment_profile).to eq( + pending_enrollment.profile, + ) + end + end + + context 'when the in_fraud_review enrollment was created more recently' do + before do + fraud_enrollment.update(created_at: Time.zone.now) + end + + it "returns the in_fraud_review enrollment's associated profile" do + expect(user.current_in_progress_in_person_enrollment_profile).to eq( + fraud_enrollment.profile, + ) + end + end + end + end end diff --git a/spec/services/form_response_spec.rb b/spec/services/form_response_spec.rb index 581d17a1e26..bb5a381d742 100644 --- a/spec/services/form_response_spec.rb +++ b/spec/services/form_response_spec.rb @@ -143,6 +143,14 @@ end describe '#to_h' do + context 'when errors is default value' do + it 'returns a hash with success key' do + response = FormResponse.new(success: true) + + expect(response.to_h).to eq(success: true) + end + end + context 'when the extra argument is nil' do it 'returns a hash with success and errors keys' do errors = { foo: 'bar' } @@ -234,25 +242,6 @@ expect(response.to_h).to eq response_hash end end - - context 'with serialize_error_details_only' do - it 'excludes errors from the hash' do - errors = ActiveModel::Errors.new(build_stubbed(:user)) - errors.add(:email_language, :blank, message: 'Language cannot be blank') - response = FormResponse.new( - success: false, - errors: errors, - serialize_error_details_only: true, - ) - - expect(response.to_h).to eq( - success: false, - error_details: { - email_language: { blank: true }, - }, - ) - end - end end end @@ -260,7 +249,7 @@ it 'allows for splatting response as alias of #to_h' do errors = ActiveModel::Errors.new(build_stubbed(:user)) errors.add(:email_language, :blank, message: 'Language cannot be blank') - response = FormResponse.new(success: false, errors:, serialize_error_details_only: true) + response = FormResponse.new(success: false, errors:) expect(**response).to eq( success: false, diff --git a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb index f3d5053aa2d..012a897fecc 100644 --- a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb +++ b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb @@ -383,28 +383,18 @@ end end - describe '#cancel_establishing_and_pending_enrollments' do - context 'when the user has an establishing in-person enrollment' do - let!(:enrollment) { create(:in_person_enrollment, :establishing, user: user) } + describe '#cancel_establishing_and_in_progress_enrollments' do + [:establishing, :pending, :in_fraud_review].each do |status| + context "when the user has an '#{status}' in-person enrollment" do + let!(:enrollment) { create(:in_person_enrollment, status, user: user) } - before do - subject.cancel_establishing_and_pending_enrollments(user) - end - - it "cancels the user's establishing in-person enrollment" do - expect(enrollment.reload.status).to eq('cancelled') - end - end - - context 'when the user has a pending in-person enrollment' do - let!(:enrollment) { create(:in_person_enrollment, :pending, user: user) } - - before do - subject.cancel_establishing_and_pending_enrollments(user) - end + before do + subject.cancel_establishing_and_in_progress_enrollments(user) + end - it "cancels the user's pending in-person enrollment" do - expect(enrollment.reload.status).to eq('cancelled') + it "cancels the user's in-person enrollment" do + expect(enrollment.reload.status).to eq('cancelled') + end end end @@ -413,7 +403,7 @@ let!(:pending_enrollment) { create(:in_person_enrollment, :pending, user: user) } before do - subject.cancel_establishing_and_pending_enrollments(user) + subject.cancel_establishing_and_in_progress_enrollments(user) end it "cancels the user's establishing in-person enrollment" do @@ -425,9 +415,9 @@ end end - context 'when the user has no establishing and pending in-person enrollments' do + context 'when the user has no establishing or in-progress in-person enrollments' do it 'does not throw an error' do - expect { subject.cancel_establishing_and_pending_enrollments(user) }.not_to raise_error + expect { subject.cancel_establishing_and_in_progress_enrollments(user) }.not_to raise_error end end end diff --git a/yarn.lock b/yarn.lock index 8c66a2fa111..56ddd043e67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1966,10 +1966,10 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.4.1" -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.3.2: version "4.3.2" @@ -2218,7 +2218,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browser-stdout@1.3.1: +browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== @@ -2320,22 +2320,7 @@ check-error@^1.0.2, check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -chokidar@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^3.4.0, chokidar@^3.6.0: +chokidar@^3.4.0, chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -2370,15 +2355,6 @@ clipboard-polyfill@^4.1.1: resolved "https://registry.yarnpkg.com/clipboard-polyfill/-/clipboard-polyfill-4.1.1.tgz#eaf074f91c0a55aa4c12fcfd4862d2cfb9a0cab9" integrity sha512-nbvNLrcX0zviek5QHLFRAaLrx8y/s8+RF2stH43tuS+kP5XlHMrcD0UGBWq43Hwp6WuuK7KefRMP56S45ibZkA== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2615,19 +2591,12 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - -debug@4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: - ms "2.1.2" + ms "^2.1.3" debug@^3.2.7: version "3.2.7" @@ -2713,15 +2682,10 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -diff@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - -diff@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== +diff@^5.0.0, diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== dir-glob@^3.0.1: version "3.0.1" @@ -2926,16 +2890,16 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-import-resolver-node@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" @@ -3273,14 +3237,6 @@ find-cache-dir@^3.3.2: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@5.0.0, find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -3296,6 +3252,14 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3452,21 +3416,10 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@^10.3.7: - version "10.4.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" - integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== +glob@^10.3.7, glob@^10.4.5: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" @@ -3612,7 +3565,7 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -he@1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -3985,7 +3938,7 @@ js-levenshtein@^1.1.6: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.1.0, js-yaml@^4.1.0: +js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -4265,7 +4218,7 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0: +log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -4384,13 +4337,6 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -minimatch@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4398,7 +4344,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: +minimatch@^5.0.1, minimatch@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -4422,43 +4368,38 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -mocha@^10.0.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.4.0.tgz#ed03db96ee9cfc6d20c56f8e2af07b961dbae261" - integrity sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA== - dependencies: - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.4" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "8.1.0" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "5.0.1" - ms "2.1.3" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - workerpool "6.2.1" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" +mocha@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.1.0.tgz#20d7c6ac4d6d6bcb60a8aa47971fca74c65c3c66" + integrity sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^10.4.5" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^17.7.2" + yargs-parser "^21.1.1" + yargs-unparser "^2.0.0" mq-polyfill@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/mq-polyfill/-/mq-polyfill-1.1.8.tgz#c144190b21214bf8d8b099e7e34e6ca2b888dc14" integrity sha1-wUQZCyEhS/jYsJnn405soriI3BQ= -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5373,14 +5314,7 @@ semver@^7.3.7, semver@^7.5.0, semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -5635,7 +5569,7 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -5731,13 +5665,6 @@ stylelint@^16.2.1: table "^6.8.1" write-file-atomic "^5.0.1" -supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -5752,6 +5679,13 @@ supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" @@ -6241,10 +6175,10 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -workerpool@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" - integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" @@ -6330,22 +6264,12 @@ yaml@^2.3.4: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-unparser@2.0.0: +yargs-unparser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== @@ -6355,19 +6279,6 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"