diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 409137aa4b6..6500200f6a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -320,13 +320,17 @@ review-app: {"name": "POSTGRES_WORKER_HOST", "value": "$CI_ENVIRONMENT_SLUG-identity-idp-chart-postgres.review-apps"}, {"name": "POSTGRES_WORKER_USERNAME", "value": "postgres"}, {"name": "POSTGRES_WORKER_PASSWORD", "value": "postgres"}, - {"name": "LOGIN_ENV", "value": "dev"}, {"name": "RAILS_OFFLINE", "value": "true"}, {"name": "REDIS_IRS_ATTEMPTS_API_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/2"}, {"name": "REDIS_THROTTLE_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/1"}, {"name": "REDIS_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379"}, {"name": "ASSET_HOST", "value": "https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}, - {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"} + {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}, + {"name": "LOGIN_DATACENTER", "value": "true" }, + {"name": "LOGIN_DOMAIN", "value": "identitysandbox.gov"}, + {"name": "LOGIN_ENV", "value": "$CI_ENVIRONMENT_SLUG" }, + {"name": "LOGIN_HOST_ROLE", "value": "idp" }, + {"name": "LOGIN_SKIP_REMOTE_CONFIG", "value": "true" } ] EOF ) @@ -343,13 +347,17 @@ review-app: {"name": "POSTGRES_WORKER_HOST", "value": "$CI_ENVIRONMENT_SLUG-identity-idp-chart-postgres.review-apps"}, {"name": "POSTGRES_WORKER_USERNAME", "value": "postgres"}, {"name": "POSTGRES_WORKER_PASSWORD", "value": "postgres"}, - {"name": "LOGIN_ENV", "value": "dev"}, {"name": "RAILS_OFFLINE", "value": "true"}, {"name": "REDIS_IRS_ATTEMPTS_API_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/2"}, {"name": "REDIS_THROTTLE_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/1"}, {"name": "REDIS_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379"}, {"name": "ASSET_HOST", "value": "https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}, - {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"} + {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}, + {"name": "LOGIN_DATACENTER", "value": "true" }, + {"name": "LOGIN_DOMAIN", "value": "identitysandbox.gov"}, + {"name": "LOGIN_ENV", "value": "$CI_ENVIRONMENT_SLUG" }, + {"name": "LOGIN_HOST_ROLE", "value": "worker" }, + {"name": "LOGIN_SKIP_REMOTE_CONFIG", "value": "true" } ] EOF ) @@ -366,6 +374,8 @@ review-app: --set-json idp.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" $CI_ENVIRONMENT_SLUG ./charts - echo "DNS may take a while to propagate, so be patient if it doesn't show up right away" + - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name review_app'" + - echo "Then run 'aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-identity-idp-chart-idp -n review-apps -- /app/bin/rails console'" environment: name: review/$CI_COMMIT_REF_NAME url: https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov diff --git a/Gemfile.lock b/Gemfile.lock index 614fd56a66a..12315d2a83e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/18F/identity-hostdata.git - revision: e33cbdb3a9c8826f6fc2b1f857fb713a4a233750 + revision: 9e2e0441cd93307cbfc5d5b8d4b3b7b4219394fb tag: v3.4.2 specs: - identity-hostdata (3.4.1) + identity-hostdata (3.4.2) activesupport (>= 6.1, < 8) aws-sdk-s3 (~> 1.8) @@ -139,17 +139,17 @@ GEM ast (2.4.2) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.684.0) + aws-partitions (1.792.0) aws-sdk-cloudwatchlogs (1.49.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.168.4) + aws-sdk-core (3.179.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.61.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.71.0) + aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) aws-sdk-pinpoint (1.62.0) aws-sdk-core (~> 3, >= 3.122.0) @@ -157,10 +157,10 @@ GEM aws-sdk-pinpointsmsvoice (1.29.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.2) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.132.0) + aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) + aws-sigv4 (~> 1.6) aws-sdk-ses (1.44.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) @@ -170,7 +170,7 @@ GEM aws-sdk-sqs (1.53.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.5.2) + aws-sigv4 (1.6.0) aws-eventstream (~> 1, >= 1.0.2) axe-core-api (4.3.2) dumb_delegator diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss index a8c626ef081..ade8a41a785 100644 --- a/app/assets/stylesheets/components/_file-input.scss +++ b/app/assets/stylesheets/components/_file-input.scss @@ -1,4 +1,5 @@ @use 'uswds-core' as *; +@use '../utilities/typography' as *; // =============================================== // Pending upstream Login Design System revisions: @@ -45,15 +46,14 @@ .usa-file-input__banner-text { @include u-font-family('sans'); + @extend %h2; color: color('primary'); display: block; - font-size: 1.625rem; letter-spacing: 0.4px; line-height: 1.5; // For content to appear as vertically centered, offset the larger line-height of the banner to // match the space below the drag text. margin-top: ((1.5rem - size('body', '2xs')) - ((1.625rem * 1.5) - 1.625rem)) * 0.5; - text-transform: uppercase; + .usa-file-input__drag-text { @include u-display('block'); diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 069f539ee9e..60d4dc22cf5 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -22,10 +22,16 @@ def call :'lg-webauthn-input', content, **tag_options, - hidden: true, - platform: platform?.presence, - 'passkey-supported-only': passkey_supported_only?.presence, + **initial_hidden_tag_options, 'show-unsupported-passkey': show_unsupported_passkey?.presence, ) end + + def initial_hidden_tag_options + if platform? && passkey_supported_only? + { hidden: true } + else + { class: 'js' } + end + end end diff --git a/app/controllers/concerns/reauthentication_required_concern.rb b/app/controllers/concerns/reauthentication_required_concern.rb index 2057bac6afa..e5e6b3291c8 100644 --- a/app/controllers/concerns/reauthentication_required_concern.rb +++ b/app/controllers/concerns/reauthentication_required_concern.rb @@ -7,7 +7,6 @@ def confirm_recently_authenticated_2fa non_remembered_device_authentication = user_session[:auth_method].present? && user_session[:auth_method] != 'remember_device' return if recently_authenticated? && non_remembered_device_authentication - return if in_multi_mfa_selection_flow? analytics.user_2fa_reauthentication_required( auth_method: user_session[:auth_method], diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 16f64a09205..82131fe8de2 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -176,7 +176,7 @@ def rate_limited_failure throttle_type: :idv_send_link, ) message = I18n.t( - 'errors.doc_auth.send_link_throttle', + 'errors.doc_auth.send_link_limited', timeout: distance_of_time_in_words( Time.zone.now, [rate_limiter.expires_at, Time.zone.now].compact.max, diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb index 7fb74026d54..e59145adf4b 100644 --- a/app/controllers/idv/in_person/ssn_controller.rb +++ b/app/controllers/idv/in_person/ssn_controller.rb @@ -35,15 +35,15 @@ def update analytics.idv_doc_auth_ssn_submitted( **analytics_arguments.merge(form_response.to_h), ) + # This event is not currently logging but should be kept as decided in LG-10110 irs_attempts_api_tracker.idv_ssn_submitted( ssn: params[:doc_auth][:ssn], ) if form_response.success? flow_session['pii_from_user'][:ssn] = params[:doc_auth][:ssn] - idv_session.invalidate_steps_after_ssn! - redirect_to next_url + redirect_to idv_in_person_verify_info_url else @error_message = form_response.first_error_message render :show, locals: extra_view_variables @@ -73,10 +73,6 @@ def confirm_repeat_ssn redirect_to idv_in_person_verify_info_url end - def next_url - idv_in_person_verify_info_url - end - def analytics_arguments { flow_path: flow_path, diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index d1f5cd6ca05..20e722786a5 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -30,7 +30,7 @@ def new Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:verify_phone, :view, true) - analytics.idv_phone_of_record_visited + analytics.idv_phone_of_record_visited(**ab_test_analytics_buckets) render :new, locals: { gpo_letter_available: gpo_letter_available } elsif async_state.missing? analytics.proofing_address_result_missing @@ -141,6 +141,7 @@ def set_idv_form previous_params: idv_session.previous_phone_step_params, allowed_countries: PhoneNumberCapabilities::ADDRESS_IDENTITY_PROOFING_SUPPORTED_COUNTRY_CODES, + failed_phone_numbers: idv_session.failed_phone_step_numbers, ) end diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 98281429d12..d9a82d95cd3 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -17,7 +17,6 @@ def show def update track_completion_event('agency-page') - irs_attempts_api_tracker.idv_reproof # if current_user.profiles&.last&.has_proofed_before? update_verified_attributes send_in_person_completion_survey if decider.go_back_to_mobile_app? diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 59c112056e8..f3cf3e06570 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -366,7 +366,7 @@ def webauthn_params def handle_too_many_confirmation_sends flash[:error] = t( - 'errors.messages.phone_confirmation_throttled', + 'errors.messages.phone_confirmation_limited', timeout: distance_of_time_in_words( Time.zone.now, [phone_confirmation_rate_limiter.expires_at, Time.zone.now].compact.max, diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index 3c9261d92df..ad1d34425b1 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -17,15 +17,16 @@ def initialize(user:, pii:, otp: nil) def submit result = valid? - threatmetrix_check_failed = fraud_review_checker.fraud_check_failed? + fraud_check_failed = pending_profile&.fraud_pending_reason.present? + if result pending_profile&.remove_gpo_deactivation_reason if pending_in_person_enrollment? UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(user, pii) pending_profile&.deactivate(:in_person_verification_pending) - elsif fraud_review_checker.fraud_check_failed? && threatmetrix_enabled? + elsif fraud_check_failed && threatmetrix_enabled? pending_profile&.deactivate_for_fraud_review - elsif fraud_review_checker.fraud_check_failed? + elsif fraud_check_failed pending_profile&.activate_after_fraud_review_unnecessary else activate_profile @@ -40,7 +41,7 @@ def submit enqueued_at: gpo_confirmation_code&.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], pending_in_person_enrollment: pending_in_person_enrollment?, - threatmetrix_check_failed: threatmetrix_check_failed, + fraud_check_failed: fraud_check_failed, }, ) end diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 71ae667d833..f6bc8c6841c 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -201,7 +201,7 @@ def limit_if_rate_limited return unless document_capture_session return unless rate_limited? - errors.add(:limit, t('errors.doc_auth.throttled_heading'), type: :throttled) + errors.add(:limit, t('errors.doc_auth.rate_limited_heading'), type: :throttled) end def track_rate_limited diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb index cb45eac0ef1..e572fb82b30 100644 --- a/app/forms/idv/phone_form.rb +++ b/app/forms/idv/phone_form.rb @@ -5,7 +5,7 @@ class PhoneForm ALL_DELIVERY_METHODS = [:sms, :voice].freeze attr_reader :user, :phone, :allowed_countries, :delivery_methods, :international_code, - :otp_delivery_preference + :otp_delivery_preference, :failed_phone_numbers validate :validate_valid_phone_for_allowed_countries validate :validate_phone_delivery_methods @@ -19,12 +19,14 @@ def initialize( user:, previous_params:, allowed_countries: nil, - delivery_methods: ALL_DELIVERY_METHODS + delivery_methods: ALL_DELIVERY_METHODS, + failed_phone_numbers: [] ) previous_params ||= {} @user = user @allowed_countries = allowed_countries @delivery_methods = delivery_methods + @failed_phone_numbers = failed_phone_numbers @international_code, @phone = determine_initial_values( **previous_params. @@ -59,9 +61,12 @@ def determine_initial_values(international_code: nil, phone: nil) international_code ||= country_code_for(phone) end - phone = PhoneFormatter.format(phone, country_code: international_code) - - [international_code, phone] + if failed_phone_numbers.include?(Phonelib.parse(phone).e164) + [nil, nil] + else + phone = PhoneFormatter.format(phone, country_code: international_code) + [international_code, phone] + end end def country_code_for(phone) diff --git a/app/forms/register_user_email_form.rb b/app/forms/register_user_email_form.rb index 17493594957..a0d1efc3ffd 100644 --- a/app/forms/register_user_email_form.rb +++ b/app/forms/register_user_email_form.rb @@ -85,6 +85,8 @@ def process_successful_submission(request_id, instructions) # already taken and if so, we act as if the user registration was successful. if email_address_record&.user&.suspended? send_suspended_user_email(email_address_record) + elsif blocked_email_address + send_suspended_user_email(blocked_email_address) elsif email_taken? && user_unconfirmed? update_user_language_preference send_sign_up_unconfirmed_email(request_id) @@ -175,4 +177,9 @@ def existing_user def email_request_id(request_id) request_id if request_id.present? && ServiceProviderRequestProxy.find_by(uuid: request_id) end + + def blocked_email_address + return @blocked_email_address if defined?(@blocked_email_address) + @blocked_email_address = SuspendedEmail.find_with_email(email) + end end diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index 6fe9f582364..f9ed09dc00e 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -8,6 +8,9 @@ class ResetPasswordForm def initialize(user) @user = user + @active_profile = user.active_profile + @pending_profile = user.pending_profile + self.reset_password_token = @user.reset_password_token end @@ -23,7 +26,7 @@ def submit(params) private - attr_reader :success + attr_reader :success, :active_profile, :pending_profile def valid_token if !user.persisted? @@ -55,11 +58,9 @@ def update_user end def mark_profile_inactive - profile = user.active_profile - return if profile.blank? + return if active_profile.blank? - @profile_deactivated = true - profile&.deactivate(:password_reset) + active_profile.deactivate(:password_reset) Funnel::DocAuth::ResetSteps.call(user.id) user.proofing_component&.destroy end @@ -82,7 +83,9 @@ def invalid_account? def extra_analytics_attributes { user_id: user.uuid, - profile_deactivated: (@profile_deactivated == true), + profile_deactivated: active_profile.present?, + pending_profile_invalidated: pending_profile.present?, + pending_profile_pending_reasons: (pending_profile&.pending_reasons || [])&.join(','), } end end diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index 9920f60ad67..456ba250c20 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -118,7 +118,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { pii: submissionError.pii, })(ReviewIssuesStep) : ReviewIssuesStep, - title: t('errors.doc_auth.throttled_heading'), + title: t('errors.doc_auth.rate_limited_heading'), }, ] as FormStep[] ).concat(inPersonSteps) diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx index 001fc0742f6..5188f106a1d 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.tsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx @@ -103,7 +103,7 @@ function ReviewIssuesStep({ return ( <> } > -

{t('errors.doc_auth.throttled_subheading')}

+

{t('errors.doc_auth.rate_limited_subheading')}

{!!unknownFieldErrors && unknownFieldErrors .filter((error) => !['front', 'back'].includes(error.field!)) diff --git a/app/javascript/packages/webauthn/index.ts b/app/javascript/packages/webauthn/index.ts index 99d4b40e820..7539dcc2507 100644 --- a/app/javascript/packages/webauthn/index.ts +++ b/app/javascript/packages/webauthn/index.ts @@ -1,6 +1,7 @@ export { default as enrollWebauthnDevice } from './enroll-webauthn-device'; export { default as extractCredentials } from './extract-credentials'; export { default as verifyWebauthnDevice } from './verify-webauthn-device'; +export { default as isExpectedWebauthnError } from './is-expected-error'; export * from './converters'; export type { VerifyCredentialDescriptor } from './verify-webauthn-device'; diff --git a/app/javascript/packages/webauthn/is-expected-error.spec.ts b/app/javascript/packages/webauthn/is-expected-error.spec.ts new file mode 100644 index 00000000000..5943091862b --- /dev/null +++ b/app/javascript/packages/webauthn/is-expected-error.spec.ts @@ -0,0 +1,24 @@ +import isExpectedWebauthnError from './is-expected-error'; + +describe('isExpectedWebauthnError', () => { + it('returns false for any error other than DOMException', () => { + const error = new TypeError(); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.false(); + }); + + it('returns false for instance of DOMException of an unexpected name', () => { + const error = new DOMException('', 'UnknownError'); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.false(); + }); + + it('returns true for instance of DOMException of an expected name', () => { + const error = new DOMException('', 'NotAllowedError'); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.true(); + }); +}); diff --git a/app/javascript/packages/webauthn/is-expected-error.ts b/app/javascript/packages/webauthn/is-expected-error.ts new file mode 100644 index 00000000000..505f5a570d4 --- /dev/null +++ b/app/javascript/packages/webauthn/is-expected-error.ts @@ -0,0 +1,12 @@ +/** + * Set of expected DOM exceptions, which occur based on some user behavior that is not noteworthy, + * such as declining permissions or timeout due to inactivity. + * + * @see https://webidl.spec.whatwg.org/#idl-DOMException + */ +const EXPECTED_DOM_EXCEPTIONS: Set = new Set(['NotAllowedError', 'TimeoutError']); + +const isExpectedWebauthnError = (error: Error): boolean => + error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name); + +export default isExpectedWebauthnError; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 18fb3e36093..7c6ee1a0ffb 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -17,73 +17,45 @@ describe('WebauthnInputElement', () => { quibble.reset(); }); - context('input for non-platform authenticator', () => { - beforeEach(() => { - document.body.innerHTML = ``; - }); + context('device does not support passkey', () => { + context('unsupported passkey not shown', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(false); + document.body.innerHTML = ``; + }); - it('becomes visible', () => { - const element = document.querySelector('lg-webauthn-input')!; + it('stays hidden', () => { + const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); + expect(element.hidden).to.be.true(); + }); }); - }); - context('input for platform authenticator', () => { - context('no passkey only restriction', () => { + context('unsupported passkey shown', () => { beforeEach(() => { - document.body.innerHTML = ``; + isWebauthnPasskeySupported.returns(false); + document.body.innerHTML = ``; }); - it('becomes visible', () => { + it('becomes visible, with modifier class', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.false(); + expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); }); }); + }); - context('passkey supported only', () => { - context('device does not support passkey', () => { - context('unsupported passkey not shown', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(false); - document.body.innerHTML = ``; - }); - - it('stays hidden', () => { - const element = document.querySelector('lg-webauthn-input')!; - - expect(element.hidden).to.be.true(); - }); - }); - - context('unsupported passkey shown', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(false); - document.body.innerHTML = ``; - }); - - it('becomes visible, with modifier class', () => { - const element = document.querySelector('lg-webauthn-input')!; - - expect(element.hidden).to.be.false(); - expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true(); - }); - }); - }); - - context('device supports passkey', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(true); - document.body.innerHTML = ``; - }); + context('device supports passkey', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(true); + document.body.innerHTML = ``; + }); - it('becomes visible', () => { - const element = document.querySelector('lg-webauthn-input')!; + it('becomes visible', () => { + const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); - }); - }); + expect(element.hidden).to.be.false(); }); }); }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 1d2fd218fb6..11938ab51a6 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -2,27 +2,23 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { - this.toggleVisibleIfSupported(); + this.toggleVisibleIfPasskeySupported(); } get isPlatform(): boolean { return this.hasAttribute('platform'); } - get isOnlyPasskeySupported(): boolean { - return this.hasAttribute('passkey-supported-only'); - } - get showUnsupportedPasskey(): boolean { return this.hasAttribute('show-unsupported-passkey'); } - isSupported(): boolean { - return !this.isPlatform || !this.isOnlyPasskeySupported || isWebauthnPasskeySupported(); - } + toggleVisibleIfPasskeySupported() { + if (!this.hasAttribute('hidden')) { + return; + } - toggleVisibleIfSupported() { - if (this.isSupported()) { + if (isWebauthnPasskeySupported()) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts index 2df8b29f64b..42f3be18738 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts @@ -6,14 +6,17 @@ import type { WebauthnVerifyButtonDataset } from './webauthn-verify-button-eleme describe('WebauthnVerifyButtonElement', () => { const verifyWebauthnDevice = sinon.stub(); + const trackError = sinon.stub(); before(async () => { quibble('./verify-webauthn-device', verifyWebauthnDevice); + quibble('@18f/identity-analytics', { trackError }); await import('./webauthn-verify-button-element'); }); beforeEach(() => { verifyWebauthnDevice.reset(); + trackError.reset(); }); after(() => { @@ -78,13 +81,33 @@ describe('WebauthnVerifyButtonElement', () => { }); }); - it('submits with error name as input on thrown error', async () => { + it('submits with error name as input on thrown expected error', async () => { + const { form } = createElement(); + + verifyWebauthnDevice.throws(new DOMException('', 'NotAllowedError')); + + const button = screen.getByRole('button', { name: 'Authenticate' }); + await userEvent.click(button); + await expect(form.submit).to.eventually.be.called(); + + expect(Object.fromEntries(new window.FormData(form))).to.deep.equal({ + credential_id: '', + authenticator_data: '', + client_data_json: '', + signature: '', + webauthn_error: 'NotAllowedError', + }); + expect(trackError).not.to.have.been.called(); + }); + + it('submits with error name as input and logs on thrown unexpected error', async () => { const { form } = createElement(); class CustomError extends Error { name = 'CustomError'; } - verifyWebauthnDevice.throws(new CustomError()); + const error = new CustomError(); + verifyWebauthnDevice.throws(error); const button = screen.getByRole('button', { name: 'Authenticate' }); await userEvent.click(button); @@ -97,6 +120,7 @@ describe('WebauthnVerifyButtonElement', () => { signature: '', webauthn_error: 'CustomError', }); + expect(trackError).to.have.been.calledWith(error); }); it('submits with verify result on successful verification', async () => { diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts index e8964761129..b876f970358 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts @@ -1,5 +1,7 @@ +import { trackError } from '@18f/identity-analytics'; import verifyWebauthnDevice from './verify-webauthn-device'; import type { VerifyCredentialDescriptor } from './verify-webauthn-device'; +import isExpectedWebauthnError from './is-expected-error'; export interface WebauthnVerifyButtonDataset extends DOMStringMap { credentials: string; @@ -50,6 +52,10 @@ class WebauthnVerifyButtonElement extends HTMLElement { this.setInputValue('client_data_json', result.clientDataJSON); this.setInputValue('signature', result.signature); } catch (error) { + if (!isExpectedWebauthnError(error)) { + trackError(error); + } + this.setInputValue('webauthn_error', error.name); } diff --git a/app/javascript/packs/idv-phone-alert.ts b/app/javascript/packs/idv-phone-alert.ts new file mode 100644 index 00000000000..0503bf6affc --- /dev/null +++ b/app/javascript/packs/idv-phone-alert.ts @@ -0,0 +1,12 @@ +import type { PhoneInputElement } from '@18f/identity-phone-input'; + +const alertElement = document.getElementById('phone-already-submitted-alert')!; +const { iti, textInput: input } = document.querySelector('lg-phone-input') as PhoneInputElement; +const failedPhoneNumbers: string[] = JSON.parse(alertElement.dataset.failedPhoneNumbers!); + +input.addEventListener('input', () => { + const isFailedPhoneNumber = failedPhoneNumbers.includes( + iti.getNumber(intlTelInputUtils.numberFormat.E164), + ); + alertElement.hidden = !isFailedPhoneNumber; +}); diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index 09fbe0fa924..64a85949d32 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -1,4 +1,10 @@ -import { enrollWebauthnDevice, extractCredentials, longToByteArray } from '@18f/identity-webauthn'; +import { + enrollWebauthnDevice, + extractCredentials, + isExpectedWebauthnError, + longToByteArray, +} from '@18f/identity-webauthn'; +import { trackError } from '@18f/identity-analytics'; import { forceRedirect } from '@18f/identity-url'; import type { Navigate } from '@18f/identity-url'; @@ -64,7 +70,13 @@ function webauthn() { result.transports.join(); (document.getElementById('webauthn_form') as HTMLFormElement).submit(); }) - .catch((err) => reloadWithError(err.name, { force: true })); + .catch((error: Error) => { + if (!isExpectedWebauthnError(error)) { + trackError(error); + } + + reloadWithError(error.name, { force: true }); + }); }); } diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index c19e0fea8b2..0d0e31b0068 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -212,6 +212,8 @@ def handle_unsupported_id_type(enrollment, response) status_check_completed_at: Time.zone.now, ) + # send SMS and email + send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), @@ -244,8 +246,10 @@ def handle_expired_status_update(enrollment, response, response_message) status: :expired, status_check_completed_at: Time.zone.now, ) - # destroy phone number for expired + + # destroy phone number for expired enrollments enrollment.notification_phone_configuration&.destroy + begin send_deadline_passed_email(enrollment.user, enrollment) unless enrollment.deadline_passed_sent rescue StandardError => err @@ -309,6 +313,9 @@ def handle_failed_status(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) + + # send SMS and email + send_enrollment_status_sms_notification(enrollment: enrollment) if response['fraudSuspected'] send_failed_fraud_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( @@ -342,6 +349,9 @@ def handle_successful_status_update(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) + + # send SMS and email + send_enrollment_status_sms_notification(enrollment: enrollment) send_verified_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), @@ -365,6 +375,8 @@ def handle_unsupported_secondary_id(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) + # send SMS and email + send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), @@ -393,9 +405,6 @@ def process_enrollment_response(enrollment, response) else handle_unsupported_status(enrollment, response) end - - # invoke job to send sms notification - send_enrollment_status_sms_notification(enrollment: enrollment) end def send_verified_email(user, enrollment) @@ -403,7 +412,7 @@ def send_verified_email(user, enrollment) # rubocop:disable IdentityIdp/MailLaterLinter UserMailer.with(user: user, email_address: email_address).in_person_verified( enrollment: enrollment, - ).deliver_later(**mail_delivery_params(enrollment.proofed_at)) + ).deliver_later(**notification_delivery_params(enrollment)) # rubocop:enable IdentityIdp/MailLaterLinter end end @@ -423,7 +432,7 @@ def send_failed_email(user, enrollment) # rubocop:disable IdentityIdp/MailLaterLinter UserMailer.with(user: user, email_address: email_address).in_person_failed( enrollment: enrollment, - ).deliver_later(**mail_delivery_params(enrollment.proofed_at)) + ).deliver_later(**notification_delivery_params(enrollment)) # rubocop:enable IdentityIdp/MailLaterLinter end end @@ -433,32 +442,33 @@ def send_failed_fraud_email(user, enrollment) # rubocop:disable IdentityIdp/MailLaterLinter UserMailer.with(user: user, email_address: email_address).in_person_failed_fraud( enrollment: enrollment, - ).deliver_later(**mail_delivery_params(enrollment.proofed_at)) + ).deliver_later(**notification_delivery_params(enrollment)) # rubocop:enable IdentityIdp/MailLaterLinter end end - def mail_delivery_params(proofed_at) - return {} if proofed_at.blank? - mail_delay_hours = IdentityConfig.store.in_person_results_delay_in_hours || - DEFAULT_EMAIL_DELAY_IN_HOURS - wait_until = proofed_at + mail_delay_hours.hours - return {} if mail_delay_hours == 0 || wait_until < Time.zone.now - return { wait_until: wait_until, queue: :intentionally_delayed } - end - # enqueue sms notification job when it's expired or success # @param [InPersonEnrollment] enrollment def send_enrollment_status_sms_notification(enrollment:) - return unless IdentityConfig.store.in_person_send_proofing_notifications_enabled - return if enrollment&.proofed_at.blank? - sms_delay_hours = IdentityConfig.store.in_person_results_delay_in_hours || - DEFAULT_EMAIL_DELAY_IN_HOURS - wait_until = enrollment.proofed_at + sms_delay_hours - InPerson::SendProofingNotificationJob.set( - wait_until: wait_until, + if IdentityConfig.store.in_person_send_proofing_notifications_enabled + InPerson::SendProofingNotificationJob.set( + **notification_delivery_params(enrollment), + ).perform_later(enrollment.id) + end + end + + def notification_delivery_params(enrollment) + return {} unless enrollment.passed? || enrollment.failed? + + wait_until = enrollment.status_check_completed_at + ( + IdentityConfig.store.in_person_results_delay_in_hours || DEFAULT_EMAIL_DELAY_IN_HOURS + ).hours + return {} unless Time.zone.now < wait_until + + { + wait_until:, queue: :intentionally_delayed, - ).perform_later(enrollment.id) + } end def email_analytics_attributes(enrollment) @@ -466,7 +476,7 @@ def email_analytics_attributes(enrollment) enrollment_code: enrollment.enrollment_code, timestamp: Time.zone.now, service_provider: enrollment.issuer, - wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until], + wait_until: notification_delivery_params(enrollment)[:wait_until], } end diff --git a/app/jobs/in_person/send_proofing_notification_job.rb b/app/jobs/in_person/send_proofing_notification_job.rb index c23bb16668d..3230af4d620 100644 --- a/app/jobs/in_person/send_proofing_notification_job.rb +++ b/app/jobs/in_person/send_proofing_notification_job.rb @@ -6,75 +6,74 @@ class SendProofingNotificationJob < ApplicationJob def perform(enrollment_id) return unless IdentityConfig.store.in_person_proofing_enabled && IdentityConfig.store.in_person_send_proofing_notifications_enabled - begin - enrollment = InPersonEnrollment.find_by( - { id: enrollment_id }, - include: [:notification_phone_configuration, :user], - ) - return unless enrollment - # skip when enrollment status not success/failed/expired and no phone configured - if enrollment.skip_notification_sent_at_set? - # log event - analytics(user: enrollment.user). - idv_in_person_usps_proofing_results_notification_job_skipped( - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, - ) - return - end - analytics(user: enrollment.user). - idv_in_person_usps_proofing_results_notification_job_started( - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, + + enrollment = InPersonEnrollment.find_by( + { id: enrollment_id }, + include: [:notification_phone_configuration, :user], + ) + + if enrollment.nil? || !enrollment.eligible_for_notification? + analytics(user: enrollment&.user || AnonymousUser.new). + idv_in_person_send_proofing_notification_job_skipped( + enrollment_code: enrollment&.enrollment_code, + enrollment_id: enrollment_id, ) - if enrollment.expired? - # no sending message for expired status - enrollment.notification_phone_configuration&.destroy - return - end + return + end - # only send sms when success or failed - # send notification and log result - phone = enrollment.notification_phone_configuration.formatted_phone - message = notification_message(enrollment: enrollment) - response = Telephony.send_notification( - to: phone, message: message, - country_code: Phonelib.parse(phone).country + analytics(user: enrollment.user). + idv_in_person_send_proofing_notification_job_started( + enrollment_code: enrollment.enrollment_code, + enrollment_id: enrollment.id, ) - handle_telephony_result(enrollment: enrollment, phone: phone, telephony_result: response) - # if notification sent successful - enrollment.update(notification_sent_at: Time.zone.now) if response.success? - ensure - Rails.logger.error("Unknown enrollment with id #{enrollment_id}") unless enrollment.present? - analytics(user: enrollment.present? ? enrollment.user : AnonymousUser.new). - idv_in_person_usps_proofing_results_notification_job_completed( - enrollment_code: enrollment&.enrollment_code, enrollment_id: enrollment_id, - ) + if enrollment.expired? + # no sending message for expired status + enrollment.notification_phone_configuration&.destroy + log_job_completed(enrollment: enrollment) + return end + + # send notification and log result when success or failed + phone = enrollment.notification_phone_configuration.formatted_phone + message = notification_message(enrollment: enrollment) + response = Telephony.send_notification( + to: phone, message: message, + country_code: Phonelib.parse(phone).country + ) + handle_telephony_response(enrollment: enrollment, phone: phone, telephony_response: response) + + enrollment.update(notification_sent_at: Time.zone.now) if response.success? + + log_job_completed(enrollment: enrollment) + rescue StandardError => err + analytics(user: enrollment&.user || AnonymousUser.new). + idv_in_person_send_proofing_notification_job_exception( + enrollment_code: enrollment&.code, + enrollment_id: enrollment_id, + exception_class: err.class.to_s, + exception_message: err.message, + ) end private - def handle_telephony_result(enrollment:, phone:, telephony_result:) - if telephony_result.success? - analytics(user: enrollment.user). - idv_in_person_usps_proofing_results_notification_sent_attempted( - success: true, - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, - telephony_result: telephony_result, - ) - else - analytics(user: enrollment.user). - idv_in_person_usps_proofing_results_notification_sent_attempted( - success: false, - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, - telephony_result: telephony_result, - ) - if telephony_result.error&.is_a?(Telephony::OptOutError) - PhoneNumberOptOut.mark_opted_out(phone) - end + def log_job_completed(enrollment:) + analytics(user: enrollment.user). + idv_in_person_send_proofing_notification_job_completed( + enrollment_code: enrollment.enrollment_code, enrollment_id: enrollment.id, + ) + end + + def handle_telephony_response(enrollment:, phone:, telephony_response:) + analytics(user: enrollment.user). + idv_in_person_send_proofing_notification_attempted( + success: telephony_response.success?, + enrollment_code: enrollment.enrollment_code, + enrollment_id: enrollment.id, + telephony_response: telephony_response.to_h, + ) + if telephony_response.error&.is_a?(Telephony::OptOutError) + PhoneNumberOptOut.mark_opted_out(phone) end end diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 967c5fe9011..b451f4afe04 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -6,6 +6,9 @@ class EmailAddress < ApplicationRecord belongs_to :user, inverse_of: :email_addresses validates :encrypted_email, presence: true validates :email_fingerprint, presence: true + # rubocop:disable Rails/HasManyOrHasOneDependent + has_one :suspended_email + # rubocop:enable Rails/HasManyOrHasOneDependent scope :confirmed, -> { where('confirmed_at IS NOT NULL') } diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 265cd342fff..ba51468cddd 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -133,8 +133,9 @@ def on_notification_sent_at_updated end end - def skip_notification_sent_at_set? - !notification_phone_configuration.present? || (!self.passed? && !self.failed? && !self.expired?) + def eligible_for_notification? + self.notification_phone_configuration.present? && + (self.passed? || self.failed? || self.expired?) end private diff --git a/app/models/profile.rb b/app/models/profile.rb index 727fe46991c..5cc0c49c2a1 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -33,7 +33,7 @@ class Profile < ApplicationRecord attr_reader :personal_key def fraud_review_pending? - fraud_pending_reason.present? && !fraud_rejection? + fraud_review_pending_at.present? end def fraud_rejection? @@ -147,26 +147,6 @@ def deactivate_for_gpo_verification end def deactivate_for_fraud_review - ## - # This is temporary. We are working on changing the way fraud review status - # is computed. The goal is that a profile is only in fraud review when - # `fraud_review_pending_at` is set. We will set this immediatly if a user - # verifies with phone and when a user enters their GPO code. - # - # We currently look at `fraud_pending_reason` to determine if a user is in - # fraud review. This allows us to change the writes on - # `fraud_review_pending_at` without side-effects. - # - # Once the writes on `fraud_review_pending_at` are correct we can move the - # reads to determine a user is fraud review pending to that column. At that - # point we can set `fraud_pending_reason` when we create a profile and - # deactivate the profile at the appropriate time for the given context - # (i.e. immediatly for phone and after GPO code entry for GPO). - # - if fraud_pending_reason.nil? - raise 'Attempting to deactivate a profile with a nil fraud pending reason' - end - update!( active: false, fraud_review_pending_at: Time.zone.now, @@ -244,10 +224,6 @@ def includes_phone_check? proofing_components['address_check'] == 'lexis_nexis_address' end - def has_proofed_before? - Profile.where(user_id: user_id).where.not(activated_at: nil).where.not(id: self.id).exists? - end - def irs_attempts_api_tracker @irs_attempts_api_tracker ||= IrsAttemptsApi::Tracker.new end diff --git a/app/models/suspended_email.rb b/app/models/suspended_email.rb new file mode 100644 index 00000000000..2e0005c4c7e --- /dev/null +++ b/app/models/suspended_email.rb @@ -0,0 +1,22 @@ +class SuspendedEmail < ApplicationRecord + belongs_to :email_address + validates :digested_base_email, presence: true + + class << self + def generate_email_digest(email) + normalized_email = EmailNormalizer.new(email).normalized_email + OpenSSL::Digest::SHA256.hexdigest(normalized_email) + end + + def create_from_email_adddress!(email_address) + create!( + digested_base_email: generate_email_digest(email_address.email), + email_address: email_address, + ) + end + + def find_with_email(email) + find_by(digested_base_email: generate_email_digest(email))&.email_address + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d132e59ef93..86f4d283bc8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -122,6 +122,9 @@ def suspend! OutOfBandSessionAccessor.new(unique_session_id).destroy if unique_session_id update!(suspended_at: Time.zone.now, unique_session_id: nil) analytics.user_suspended(success: true) + email_addresses.map do |email_address| + SuspendedEmail.create_from_email_adddress!(email_address) + end end def reinstate! @@ -131,6 +134,9 @@ def reinstate! end update!(reinstated_at: Time.zone.now) analytics.user_reinstated(success: true) + email_addresses.map do |email_address| + SuspendedEmail.find_with_email(email_address.email)&.destroy + end end def pending_profile @@ -140,7 +146,9 @@ def pending_profile pending = profiles.where(deactivation_reason: :in_person_verification_pending).or( profiles.where.not(gpo_verification_pending_at: nil), ).or( - profiles.where.not(fraud_pending_reason: nil), + profiles.where.not(fraud_review_pending_at: nil), + ).or( + profiles.where.not(fraud_rejection_at: nil), ).order(created_at: :desc).first if pending.blank? diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 8d688615031..5ee0e3a5d62 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1464,6 +1464,103 @@ def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra) ) end + # Track sms notification attempt + # @param [boolean] success sms notification successful or not + # @param [String] enrollment_code enrollment_code + # @param [String] enrollment_id enrollment_id + # @param [Hash] telephony_response response from Telephony gem + # @param [Hash] extra extra information + def idv_in_person_send_proofing_notification_attempted( + success:, + enrollment_code:, + enrollment_id:, + telephony_response:, + **extra + ) + track_event( + 'IdV: in person notification SMS send attempted', + success: success, + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + telephony_response: telephony_response, + **extra, + ) + end + + # Track sms notification job completion + # @param [String] enrollment_code enrollment_code + # @param [String] enrollment_id enrollment_id + # @param [Hash] extra extra information + def idv_in_person_send_proofing_notification_job_completed( + enrollment_code:, + enrollment_id:, + **extra + ) + track_event( + 'SendProofingNotificationAndDeletePhoneNumberJob: job completed', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + **extra, + ) + end + + # Tracks exceptions that are raised when running InPerson::SendProofingNotificationJob + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + # @param [Hash] extra extra information + def idv_in_person_send_proofing_notification_job_exception( + enrollment_code:, + enrollment_id:, + exception_class: nil, + exception_message: nil, + **extra + ) + track_event( + 'SendProofingNotificationJob: Exception raised', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + **extra, + ) + end + + # Track sms notification job skipped + # @param [String] enrollment_code enrollment_code + # @param [String] enrollment_id enrollment_id + # @param [Hash] extra extra information + def idv_in_person_send_proofing_notification_job_skipped( + enrollment_code:, + enrollment_id:, + **extra + ) + track_event( + 'SendProofingNotificationAndDeletePhoneNumberJob: job skipped', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + **extra, + ) + end + + # Track sms notification job started + # @param [String] enrollment_code enrollment_code + # @param [String] enrollment_id enrollment_id + # @param [Hash] extra extra information + def idv_in_person_send_proofing_notification_job_started( + enrollment_code:, + enrollment_id:, + **extra + ) + track_event( + 'SendProofingNotificationAndDeletePhoneNumberJob: job started', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + **extra, + ) + end + # @param [String] flow_path Document capture path ("hybrid" or "standard") # The user submitted the in person proofing switch_back step def idv_in_person_switch_back_submitted(flow_path:, **extra) @@ -1722,76 +1819,6 @@ def idv_in_person_usps_proofing_results_job_unexpected_response( ) end - # Track sms notification job completion - # @param [String] enrollment_code enrollment_code - # @param [String] enrollment_id enrollment_id - # @param [Hash] extra extra information - def idv_in_person_usps_proofing_results_notification_job_completed(enrollment_code:, - enrollment_id:, - **extra) - track_event( - 'SendProofingNotificationAndDeletePhoneNumberJob: job completed', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - **extra, - ) - end - - # Track sms notification job skipped - # @param [String] enrollment_code enrollment_code - # @param [String] enrollment_id enrollment_id - # @param [Hash] extra extra information - def idv_in_person_usps_proofing_results_notification_job_skipped( - enrollment_code:, - enrollment_id:, - **extra - ) - track_event( - 'SendProofingNotificationAndDeletePhoneNumberJob: job skipped', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - **extra, - ) - end - - # Track sms notification job started - # @param [String] enrollment_code enrollment_code - # @param [String] enrollment_id enrollment_id - # @param [Hash] extra extra information - def idv_in_person_usps_proofing_results_notification_job_started(enrollment_code:, - enrollment_id:, - **extra) - track_event( - 'SendProofingNotificationAndDeletePhoneNumberJob: job started', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - **extra, - ) - end - - # Track sms notification attempt - # @param [boolean] success sms notification successful or not - # @param [String] enrollment_code enrollment_code - # @param [String] enrollment_id enrollment_id - # @param [Telephony::Response] telephony_result - # @param [Hash] extra extra information - def idv_in_person_usps_proofing_results_notification_sent_attempted( - success:, - enrollment_code:, - enrollment_id:, - telephony_result:, - **extra - ) - track_event( - 'IdV: in person notification SMS send attempted', - success: success, - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - telephony_result: telephony_result, - **extra, - ) - end - # Tracks if USPS in-person proofing enrollment request fails # @param [String] context # @param [String] reason diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index caf6f1fc4c9..d5f1959a31d 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -43,7 +43,11 @@ def async_state_done(async_state) @idv_result = async_state.result success = idv_result[:success] - handle_successful_proofing_attempt if success + if success + handle_successful_proofing_attempt + else + handle_failed_proofing_attempt + end delete_async FormResponse.new( @@ -170,5 +174,11 @@ def missing def delete_async idv_session.idv_phone_step_document_capture_session_uuid = nil end + + def handle_failed_proofing_attempt + return if failure_reason == :timeout + + idv_session.add_failed_phone_step_number(idv_session.previous_phone_step_params[:phone]) + end end end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 48b12bfbb5d..8cbef61007c 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -128,6 +128,16 @@ def user_phone_confirmation_session=(new_user_phone_confirmation_session) session[:user_phone_confirmation_session] = new_user_phone_confirmation_session.to_h end + def failed_phone_step_numbers + session[:failed_phone_step_params] ||= [] + end + + def add_failed_phone_step_number(phone) + parsed_phone = Phonelib.parse(phone) + phone_e164 = parsed_phone.e164 + failed_phone_step_numbers << phone_e164 if !failed_phone_step_numbers.include?(phone_e164) + end + def in_person_enrollment? ProofingComponent.find_by(user: current_user)&.document_check == Idp::Constants::Vendors::USPS end diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 05563836e3a..2b141e89ad6 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -61,7 +61,7 @@ def rate_limited_response redirect_to rate_limited_url DocAuth::Response.new( success: false, - errors: { limit: I18n.t('errors.doc_auth.throttled_heading') }, + errors: { limit: I18n.t('errors.doc_auth.rate_limited_heading') }, ) end diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb index 9179bb36ad2..259b34f31bb 100644 --- a/app/services/irs_attempts_api/tracker_events.rb +++ b/app/services/irs_attempts_api/tracker_events.rb @@ -276,15 +276,6 @@ def idv_phone_upload_link_used ) end - # The user, who had previously successfully confirmed their identity, has - # reproofed. All the normal events are also sent, this simply notes that - # this is the second (or more) time they have gone through the process successfully. - def idv_reproof - track_event( - :idv_reproof, - ) - end - # @param [String] ssn # User entered in SSN number during Identity verification def idv_ssn_submitted(ssn:) diff --git a/app/services/maintenance_window.rb b/app/services/maintenance_window.rb deleted file mode 100644 index b2a01fdbbd2..00000000000 --- a/app/services/maintenance_window.rb +++ /dev/null @@ -1,13 +0,0 @@ -class MaintenanceWindow - attr_reader :start, :finish, :now - - def initialize(start:, finish:, now: nil, display_time_zone: 'America/New_York') - @start = start.in_time_zone(display_time_zone) if start.present? - @finish = finish.in_time_zone(display_time_zone) if finish.present? - @now = now || Time.zone.now - end - - def active? - (start...finish).cover?(now) if start && finish - end -end diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 051a3b8fc2d..18946fc61b8 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,5 +1,4 @@ <% title t('titles.visitors.index') %> -<%= render 'shared/maintenance_window_alert' %> <% if decorated_session.sp_name %> <%= render 'sign_up/registrations/sp_registration_heading' %> diff --git a/app/views/idv/getting_started/show.html.erb b/app/views/idv/getting_started/show.html.erb index 27c0bad7f64..ddc9be31c11 100644 --- a/app/views/idv/getting_started/show.html.erb +++ b/app/views/idv/getting_started/show.html.erb @@ -1,91 +1,89 @@ <% title @title %> -<%= render 'shared/maintenance_window_alert' do %> - <%= render JavascriptRequiredComponent.new( - header: t('idv.getting_started.no_js_header'), - intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name), - ) do %> +<%= render JavascriptRequiredComponent.new( + header: t('idv.getting_started.no_js_header'), + intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name), + ) do %> - <%= render AlertComponent.new( - type: :error, - class: [ - 'js-consent-form-alert', - 'margin-bottom-4', - flow_session[:error_message].blank? && 'display-none', - ].select(&:present?), - message: flow_session[:error_message].presence || t('errors.doc_auth.consent_form'), - ) %> +<%= render AlertComponent.new( + type: :error, + class: [ + 'js-consent-form-alert', + 'margin-bottom-4', + flow_session[:error_message].blank? && 'display-none', + ].select(&:present?), + message: flow_session[:error_message].presence || t('errors.doc_auth.consent_form'), + ) %> - <%= render PageHeadingComponent.new.with_content(@title) %> -

- <%= t( - 'doc_auth.info.getting_started_html', - sp_name: @sp_name, - link_html: new_tab_link_to( - t('doc_auth.info.getting_started_learn_more'), - help_center_redirect_path( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - flow: :idv, - step: :getting_started, - location: 'intro_paragraph', - ), +<%= render PageHeadingComponent.new.with_content(@title) %> +

+ <%= t( + 'doc_auth.info.getting_started_html', + sp_name: @sp_name, + link_html: new_tab_link_to( + t('doc_auth.info.getting_started_learn_more'), + help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-verify-your-identity', + flow: :idv, + step: :getting_started, + location: 'intro_paragraph', ), - ) %> -

+ ), + ) %> +

-

<%= t('doc_auth.getting_started.instructions.getting_started') %>

+

<%= t('doc_auth.getting_started.instructions.getting_started') %>

- <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %> -

<%= t('doc_auth.getting_started.instructions.text1') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %> -

<%= t('doc_auth.getting_started.instructions.text2') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %> -

<%= t('doc_auth.getting_started.instructions.text3') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %> -

<%= t('doc_auth.getting_started.instructions.text4') %>

- <% end %> + <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> + <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %> +

<%= t('doc_auth.getting_started.instructions.text1') %>

<% end %> - - <%= simple_form_for( - :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, - ) do |f| %> - <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %> - <%= render ValidatedFieldComponent.new( - form: f, - name: :ial2_consent_given, - as: :boolean, - label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME), - required: true, - ) %> + <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %> +

<%= t('doc_auth.getting_started.instructions.text2') %>

+ <% end %> + <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %> +

<%= t('doc_auth.getting_started.instructions.text3') %>

+ <% end %> + <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %> +

<%= t('doc_auth.getting_started.instructions.text4') %>

<% end %> -

- <%= new_tab_link_to( - t('doc_auth.getting_started.instructions.learn_more'), - policy_redirect_url(flow: :idv, step: :getting_started, location: :consent), - ) %> -

-
- <%= render( - SpinnerButtonComponent.new( - type: :submit, - big: true, - wide: true, - spin_on_click: false, - ).with_content(t('doc_auth.buttons.continue')), - ) %> -
<% end %> - <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %> +<%= simple_form_for( + :doc_auth, + url: url_for, + method: 'put', + html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, + ) do |f| %> + <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %> + <%= render ValidatedFieldComponent.new( + form: f, + name: :ial2_consent_given, + as: :boolean, + label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME), + required: true, + ) %> <% end %> +

+ <%= new_tab_link_to( + t('doc_auth.getting_started.instructions.learn_more'), + policy_redirect_url(flow: :idv, step: :getting_started, location: :consent), + ) %> +

+
+ <%= render( + SpinnerButtonComponent.new( + type: :submit, + big: true, + wide: true, + spin_on_click: false, + ).with_content(t('doc_auth.buttons.continue')), + ) %> +
+<% end %> + + <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %> <% end %> <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/idv/gpo_verify/throttled.html.erb b/app/views/idv/gpo_verify/throttled.html.erb index cc638e2b350..07026775dad 100644 --- a/app/views/idv/gpo_verify/throttled.html.erb +++ b/app/views/idv/gpo_verify/throttled.html.erb @@ -4,7 +4,7 @@

<%= t( - 'errors.verify_profile.throttled', + 'errors.verify_profile.rate_limited', timeout: distance_of_time_in_words( Time.zone.now, [@expires_at, Time.zone.now].compact.max, diff --git a/app/views/idv/phone/new.html.erb b/app/views/idv/phone/new.html.erb index d1454b85c95..8676e1d388a 100644 --- a/app/views/idv/phone/new.html.erb +++ b/app/views/idv/phone/new.html.erb @@ -65,6 +65,26 @@ required: true, class: 'margin-bottom-4', ) %> + <%= render AlertComponent.new( + type: :warning, + class: 'margin-bottom-4', + id: 'phone-already-submitted-alert', + data: { + failed_phone_numbers: @idv_form.failed_phone_numbers, + }, + hidden: true, + ) do %> + <%= t('idv.messages.phone.failed_number.alert_text') %> + <% if gpo_letter_available %> + <%= t( + 'idv.messages.phone.failed_number.gpo_alert_html', + link_html: link_to( + t('idv.messages.phone.failed_number.gpo_verify_link'), + idv_gpo_path, + ), + ) %> + <% end %> + <% end %>

<%= t('idv.titles.otp_delivery_method') %>

<%= t('idv.messages.otp_delivery_method_description') %>

@@ -115,4 +135,4 @@ <%= render 'idv/doc_auth/cancel', step: 'phone' %> -<% javascript_packs_tag_once 'form-steps-wait' %> +<% javascript_packs_tag_once 'form-steps-wait', 'idv-phone-alert' %> diff --git a/app/views/idv/session_errors/throttled.html.erb b/app/views/idv/session_errors/throttled.html.erb index ae644d260d0..e7fe95ac22d 100644 --- a/app/views/idv/session_errors/throttled.html.erb +++ b/app/views/idv/session_errors/throttled.html.erb @@ -1,6 +1,6 @@ <%= render( 'idv/shared/error', - heading: t('errors.doc_auth.throttled_heading'), + heading: t('errors.doc_auth.rate_limited_heading'), options: [ { url: MarketingSite.contact_url, @@ -11,7 +11,7 @@ ) do %>

<%= t( - 'errors.doc_auth.throttled_text_html', + 'errors.doc_auth.rate_limited_text_html', timeout: distance_of_time_in_words( Time.zone.now, [@expires_at, Time.zone.now].compact.max, diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 788a36fbb85..1c516a5a7bb 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -9,7 +9,6 @@ ) %> <% end %> -<%= render 'shared/maintenance_window_alert' do %> <%= render JavascriptRequiredComponent.new( header: t('idv.welcome.no_js_header'), intro: t('idv.welcome.no_js_intro', sp_name: decorated_session.sp_name || APP_NAME), @@ -55,54 +54,53 @@ <%= f.submit t('doc_auth.buttons.continue') %> <% end %> - <%= render( - 'shared/troubleshooting_options', - heading_tag: :h3, - heading: t('idv.troubleshooting.headings.missing_required_items'), - options: [ - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'accepted-state-issued-identification', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.supported_documents'), - new_tab: true, - }, - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'phone-number', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.learn_more_address_verification_options'), - new_tab: true, - }, - decorated_session.sp_name && { - url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), - text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_session.sp_name), - new_tab: true, - }, - ].select(&:present?), - ) %> + <%= render( + 'shared/troubleshooting_options', + heading_tag: :h3, + heading: t('idv.troubleshooting.headings.missing_required_items'), + options: [ + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + flow: :idv, + step: :welcome, + location: 'missing_items', + ), + text: t('idv.troubleshooting.options.supported_documents'), + new_tab: true, + }, + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'phone-number', + flow: :idv, + step: :welcome, + location: 'missing_items', + ), + text: t('idv.troubleshooting.options.learn_more_address_verification_options'), + new_tab: true, + }, + decorated_session.sp_name && { + url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), + text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_session.sp_name), + new_tab: true, + }, + ].select(&:present?), + ) %> -

<%= t('doc_auth.instructions.privacy') %>

-

- <%= t('doc_auth.info.privacy', app_name: APP_NAME) %> -

-

- <%= new_tab_link_to( - t('doc_auth.instructions.learn_more'), - policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) %> -

+

<%= t('doc_auth.instructions.privacy') %>

+

+ <%= t('doc_auth.info.privacy', app_name: APP_NAME) %> +

+

+ <%= new_tab_link_to( + t('doc_auth.instructions.learn_more'), + policy_redirect_url(flow: :idv, step: :welcome, location: :footer), + ) %> +

- <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> - <% end %> + <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> <% end %> <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/shared/_maintenance_window_alert.html.erb b/app/views/shared/_maintenance_window_alert.html.erb deleted file mode 100644 index b1d78a5a02f..00000000000 --- a/app/views/shared/_maintenance_window_alert.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<% maintenance_window = MaintenanceWindow.new( - start: IdentityConfig.store.acuant_maintenance_window_start, - finish: IdentityConfig.store.acuant_maintenance_window_finish, - now: local_assigns[:now], - ) %> -<% if maintenance_window.active? %> - <%= render AlertComponent.new(type: :warning, class: 'margin-bottom-2', text_tag: 'div') do %> -

- <%= t( - 'notices.maintenance.currently_under_maintenance_html', - finish: l( - maintenance_window.finish, - format: t('time.formats.event_timestamp_with_zone'), - ), - ) %> -

-

- <%= t('notices.maintenance.need_assistance') %> - <%= link_to(t('notices.maintenance.contact_us'), MarketingSite.contact_url) %> -

- <% end %> -<% else %> - <%= yield %> -<% end %> diff --git a/app/views/users/verify_personal_key/throttled.html.erb b/app/views/users/verify_personal_key/throttled.html.erb index 83660193e0c..c20e71f6cdb 100644 --- a/app/views/users/verify_personal_key/throttled.html.erb +++ b/app/views/users/verify_personal_key/throttled.html.erb @@ -4,7 +4,7 @@

<%= t( - 'errors.verify_personal_key.throttled', + 'errors.verify_personal_key.rate_limited', timeout: distance_of_time_in_words( Time.zone.now, [@expires_at, Time.zone.now].compact.max, diff --git a/config/application.yml.default b/config/application.yml.default index b4b4fb48b11..10c9623bf9e 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -26,8 +26,6 @@ allowed_ialmax_providers: '[]' account_reset_token_valid_for_days: 1 account_reset_wait_period_days: 1 account_suspended_support_code: EFGHI -acuant_maintenance_window_start: -acuant_maintenance_window_finish: acuant_assure_id_password: '' acuant_assure_id_subscription_id: '' acuant_assure_id_url: '' @@ -139,7 +137,6 @@ in_person_email_reminder_early_benchmark_in_days: 11 in_person_email_reminder_final_benchmark_in_days: 1 in_person_email_reminder_late_benchmark_in_days: 4 in_person_proofing_enabled: false -in_person_send_proofing_notifications_enabled: false in_person_enrollment_validity_in_days: 30 in_person_enrollments_ready_job_email_body_pattern: '\A\s*(?\d{16})\s*\Z' in_person_enrollments_ready_job_cron: '0/10 * * * *' diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index d9143e5f5ca..33e855c2f84 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -119,17 +119,17 @@ en: text4: Your password saves and encrypts your personal information. headings: address: Update your mailing address - back: Back + back: Back of your driver’s license or state ID capture_complete: We verified your ID capture_scan_warning_html: We couldn’t read the barcode on your ID. If the information below is incorrect, please %{link_html} of your state‑issued ID. capture_scan_warning_link: upload new photos capture_troubleshooting_tips: Having trouble adding your state‑issued ID? - document_capture: Add your state‑issued ID + document_capture: Add photos of your ID document_capture_back: Back of your ID document_capture_front: Front of your ID - front: Front + front: Front of your driver’s license or state ID getting_started: Let’s verify your identity for %{sp_name} hybrid_handoff: How would you like to add your ID? interstitial: We are processing your images @@ -162,8 +162,8 @@ en: capture_status_small_document: Move Closer capture_status_tap_to_capture: Tap to Capture document_capture_intro_acknowledgment: We’ll collect information about you by - reading your state‑issued ID. We use this information to verify your - identity. + reading your driver’s license or state ID card. We use this information + to verify your identity. getting_started_html: '%{sp_name} needs to make sure you are you — not someone pretending to be you. %{link_html}' getting_started_learn_more: Learn more about what you need to verify your identity diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index dadb1cf26c6..6789e5a856d 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -144,17 +144,17 @@ es: text4: Su contraseña guarda y encripta su información personal. headings: address: Actualice su dirección postal - back: Parte Trasera + back: Reverso de su licencia de conducir o identificación estatal capture_complete: Verificamos su identificación capture_scan_warning_html: No pudimos leer el código de barras en su ID. Si la información que aparece a continuación es incorrecta, por favor, %{link_html} de su ID emitido por el estado. capture_scan_warning_link: suba nuevas fotos capture_troubleshooting_tips: '¿Tiene problemas para agregar su identificación emitida por el estado?' - document_capture: Añada su documento de identidad expedido por el estado + document_capture: Incluir fotos de su identificación document_capture_back: Parte trasera de su documento de identidad document_capture_front: Parte delantera de su documento de identidad - front: Parte Delantera + front: Anverso de su licencia de conducir o identificación estatal getting_started: Vamos a verificar su identidad para %{sp_name} hybrid_handoff: '¿Cómo desea añadir su documento de identidad?' interstitial: Estamos procesando sus imágenes @@ -190,7 +190,7 @@ es: capture_status_small_document: Muévete mas cerca capture_status_tap_to_capture: Toque para capturar document_capture_intro_acknowledgment: Recopilaremos información sobre usted - leyendo su documento de identidad expedido por el Estado. Usamos esta + leyendo su licencia de conducir o identificación estatal. Usamos esta información para verificar su identidad. getting_started_html: '%{sp_name} necesita asegurarse de que es usted realmente y no alguien que se hace pasar por usted. %{link_html}' diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 42d037b6a46..d0c7da71452 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -150,17 +150,17 @@ fr: text4: Votre mot de passe sauvegarde et crypte vos informations personnelles. headings: address: Mettre à jour votre adresse postale - back: Verso + back: Verso de votre permis de conduire ou de votre carte d’identité de l’État capture_complete: Nous avons vérifié votre document d’identité capture_scan_warning_html: Nous n’avons pas pu lire le code-barres de votre pièce d’identité. Si les informations ci-dessous sont incorrectes, veuillez %{link_html} de votre carte d’identité délivrée par l’État. capture_scan_warning_link: télécharger de nouvelles photos capture_troubleshooting_tips: Vous rencontrez des difficultés pour ajouter votre pièce d’identité? - document_capture: Ajoutez votre carte d’identité délivrée par l’État + document_capture: Ajoutez des photos de votre pièce d’identité document_capture_back: Verso de votre carte d’identité document_capture_front: Recto de votre carte d’identité - front: Recto + front: Recto de votre permis de conduire ou de votre carte d’identité de l’État getting_started: Vérifions votre identité pour %{sp_name} hybrid_handoff: Comment voulez-vous ajouter votre identifiant ? interstitial: Nous traitons vos images @@ -195,8 +195,8 @@ fr: capture_status_small_document: Approchez-vous capture_status_tap_to_capture: Appuyez pour capturer document_capture_intro_acknowledgment: Nous recueillons des informations sur - vous en lisant votre pièce d’identité délivrée par l’État. Nous - utilisons ces informations pour vérifier votre identité. + vous en lisant votre permis de conduire ou votre carte d’identité de + l’État. Nous utilisons ces informations pour vérifier votre identité. getting_started_html: '%{sp_name} doit s’assurer que c’est bien vous — et non quelqu’un qui se fait passer pour vous. %{link_html}' getting_started_learn_more: En savoir plus sur ce dont vous avez besoin pour vérifier votre identité diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 3600ddcd5c9..b13751eaba5 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -27,13 +27,13 @@ en: document_capture_cancelled: You have cancelled uploading photos of your ID on your phone. phone_step_incomplete: You must go to your phone and upload photos of your ID before continuing. We sent you a link with instructions. - send_link_throttle: You tried too many times, please try again in %{timeout}. - You can also go back and choose to use your computer instead. - throttled_heading: We couldn’t verify your ID - throttled_subheading: Try taking new pictures - throttled_text_html: 'For your security, we limit the number of times you can + rate_limited_heading: We couldn’t verify your ID + rate_limited_subheading: Try taking new pictures + rate_limited_text_html: 'For your security, we limit the number of times you can attempt to verify a document online. Try again in %{timeout}.' + send_link_limited: You tried too many times, please try again in %{timeout}. You + can also go back and choose to use your computer instead. general: Oops, something went wrong. Please try again. invalid_totp: Invalid code. Please try again. max_password_attempts_reached: You’ve entered too many incorrect passwords. You @@ -71,7 +71,7 @@ en: personal_key_incorrect: Incorrect personal key phone_carrier: Sorry, we are unable to support that phone carrier at this time. Please select a different number and try again. - phone_confirmation_throttled: You tried too many times, please try again in %{timeout}. + phone_confirmation_limited: You tried too many times, please try again in %{timeout}. phone_duplicate: This account is already using the phone number you entered as an authenticator. Please use a different phone number. phone_required: Phone number is required @@ -110,9 +110,9 @@ en: must_select_additional_option: Select an additional authentication method. must_select_option: Select an authentication method. verify_personal_key: - throttled: You tried too many times, please try again in %{timeout}. + rate_limited: You tried too many times, please try again in %{timeout}. verify_profile: - throttled: You tried too many times, please try again in %{timeout}. + rate_limited: You tried too many times, please try again in %{timeout}. webauthn_platform_setup: account_setup_error: We were unable to add face or touch unlock. Please try again or %{link}. already_registered: Face or touch unlock is already registered on this device. diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index 80355dc3322..b9104098488 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -28,14 +28,14 @@ es: document_capture_cancelled: Ha cancelado la carga de fotos de su identificación en este teléfono. phone_step_incomplete: Debe ir a su teléfono y cargar fotos de su identificación antes de continuar. Te enviamos un enlace con instrucciones. - send_link_throttle: Ha intentado demasiadas veces, por favor, inténtelo de nuevo + rate_limited_heading: No pudimos verificar la identificación + rate_limited_subheading: Intente tomar nuevas fotografías. + rate_limited_text_html: 'Por su seguridad, limitamos el número de veces que + puede intentar verificar un documento en línea. Inténtelo de + nuevo en %{timeout}.' + send_link_limited: Ha intentado demasiadas veces, por favor, inténtelo de nuevo en %{timeout}. También puede retroceder y elegir utilizar su computadora como alternativa. - throttled_heading: No pudimos verificar la identificación - throttled_subheading: Intente tomar nuevas fotografías. - throttled_text_html: 'Por su seguridad, limitamos el número de veces que puede - intentar verificar un documento en línea. Inténtelo de nuevo en - %{timeout}.' general: '¡Oops! Algo salió mal. Inténtelo de nuevo.' invalid_totp: El código es inválido. Vuelva a intentarlo. max_password_attempts_reached: Ha ingresado demasiadas contraseñas incorrectas. @@ -75,7 +75,7 @@ es: personal_key_incorrect: La clave personal es incorrecta phone_carrier: Lo sentimos, no podemos admitir ese operador telefónico en este momento. Por favor, seleccione un número diferente e inténtelo de nuevo. - phone_confirmation_throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. + phone_confirmation_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. phone_duplicate: Esta cuenta ya está utilizando el número de teléfono que ingresó como autenticador. Por favor, use un número de teléfono diferente. @@ -116,9 +116,9 @@ es: must_select_additional_option: Seleccione un método de autenticación adicional. must_select_option: Seleccione un método de autenticación. verify_personal_key: - throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. + rate_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. verify_profile: - throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. + rate_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}. webauthn_platform_setup: account_setup_error: No pudimos agregar el desbloqueo con la cara o con la huella digital. Inténtelo de nuevo o %{link}. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index abe7843e95a..d1febdff5b2 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -32,14 +32,14 @@ fr: phone_step_incomplete: Vous devez aller sur votre téléphone et télécharger des photos de votre identifiant avant de continuer. Nous vous avons envoyé un lien avec des instructions. - send_link_throttle: Vous avez essayé trop de fois, veuillez réessayer dans - %{timeout}. Vous pouvez également revenir en arrière et choisir - d’utiliser votre ordinateur à la place. - throttled_heading: Nous n’avons pas pu vérifier votre identité - throttled_subheading: Essayez de prendre de nouvelles photos. - throttled_text_html: 'Pour votre sécurité, nous limitons le nombre de fois où + rate_limited_heading: Nous n’avons pas pu vérifier votre identité + rate_limited_subheading: Essayez de prendre de nouvelles photos. + rate_limited_text_html: 'Pour votre sécurité, nous limitons le nombre de fois où vous pouvez tenter de vérifier un document en ligne. Veuillez réessayer dans %{timeout}.' + send_link_limited: Vous avez essayé trop de fois, veuillez réessayer dans + %{timeout}. Vous pouvez également revenir en arrière et choisir + d’utiliser votre ordinateur à la place. general: Oups, une erreur s’est produite. Veuillez essayer de nouveau. invalid_totp: Code non valide. Veuillez essayer de nouveau. max_password_attempts_reached: Vous avez inscrit des mots de passe incorrects un @@ -83,7 +83,7 @@ fr: phone_carrier: Nous nous excusons, car nous ne pouvons pas prendre en charge cet opérateur téléphonique pour le moment. Veuillez sélectionner un autre numéro puis réessayer. - phone_confirmation_throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. + phone_confirmation_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. phone_duplicate: Ce compte utilise déjà le numéro de téléphone que vous avez entré en tant qu’authentificateur. Veuillez utiliser un numéro de téléphone différent. @@ -126,9 +126,9 @@ fr: must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire. must_select_option: Sélectionnez une méthode d’authentification. verify_personal_key: - throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. + rate_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. verify_profile: - throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. + rate_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}. webauthn_platform_setup: account_setup_error: Nous n’avons pas pu ajouter le déverrouillage facial ni le déverrouillage tactile. Veuillez réessayer ou %{link}. diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 9ec71063d31..6e4099cbb4e 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -213,6 +213,11 @@ en: alert_html: 'Enter a phone number that is:' description: We’ll check this number with records and send you a one-time code. This is to help verify your identity. + failed_number: + alert_text: We couldn’t match you to this number. + gpo_alert_html: If you don’t have another number to try, + %{link_html} instead. + gpo_verify_link: verify by mail rules: - Based in the United States (including U.S. territories) - Your primary number (the one you use the most often) diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index dd8a0e31292..6a44773f345 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -224,6 +224,11 @@ es: alert_html: 'Introduzca un número de teléfono que sea:' description: Comprobaremos este número con los registros y le enviaremos un código único. Esto es para ayudar a verificar su identidad. + failed_number: + alert_text: No pudimos asociarlo a este número. + gpo_alert_html: Si no dispone de otro número de teléfono, + %{link_html}. + gpo_verify_link: realice la verificación por correo rules: - Con base en Estados Unidos (incluidos los territorios de EE.UU.) - Su número principal (el que utiliza con más frecuencia) diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 8c48a872496..f0bbd4c3cb9 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -237,6 +237,11 @@ fr: alert_html: 'Entrez un numéro de téléphone qui est :' description: Nous vérifierons ce numéro dans nos archives et vous enverrons un code à usage unique. Ceci est pour aider à vérifier votre identité. + failed_number: + alert_text: Nous n’avons pas pu vous associer à ce numéro. + gpo_alert_html: Si vous n’avez pas d’autre numéro de téléphone + à essayer, %{link_html}. + gpo_verify_link: vérifiez plutôt par courrier rules: - Basé aux Etats-Unis (y compris les territoires américains) - Votre numéro principal (celui que vous utilisez le plus souvent) diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml index 55e754195a3..9de70481ed0 100644 --- a/config/locales/notices/en.yml +++ b/config/locales/notices/en.yml @@ -16,11 +16,6 @@ en: use_diff_email: link: create a new account text_html: Or, %{link_html} using a different email address. - maintenance: - contact_us: Contact us - currently_under_maintenance_html: We are currently under maintenance until - %{finish} for some of our services. - need_assistance: Need assistance? password_changed: You changed your password. phone_confirmed: A phone was added to your account. piv_cac_configured: A PIV/CAC card was added to your account. diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml index 15f3b495a25..e9ff0e44a59 100644 --- a/config/locales/notices/es.yml +++ b/config/locales/notices/es.yml @@ -16,11 +16,6 @@ es: use_diff_email: link: crear una cuenta nueva text_html: O, %{link_html} utilizando un email diferente. - maintenance: - contact_us: Contacta con nosotros - currently_under_maintenance_html: Actualmente estamos en mantenimiento hasta - el %{finish} para algunos de nuestros servicios. - need_assistance: '¿Necesita ayuda?' password_changed: Ha cambiado su contraseña. phone_confirmed: Un teléfono fue agregado a tu cuenta. piv_cac_configured: Una tarjeta PIV/CAC fue agregada a tu cuenta. diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml index e710595be57..24e554144bc 100644 --- a/config/locales/notices/fr.yml +++ b/config/locales/notices/fr.yml @@ -16,11 +16,6 @@ fr: use_diff_email: link: Créer un nouveau compte text_html: Ou, %{link_html} en utilisant une adresse courriel différente. - maintenance: - contact_us: Nous contacter - currently_under_maintenance_html: Nous sommes actuellement en maintenance - jusqu’au %{finish} pour certains de nos services. - need_assistance: Besoin d’assistance? password_changed: Vous avez changé votre mot de passe. phone_confirmed: Un téléphone a été ajouté à votre compte. piv_cac_configured: Une carte PIV / CAC a été ajoutée à votre compte. diff --git a/config/locales/telephony/en.yml b/config/locales/telephony/en.yml index f8770beff4f..9e02a0167f4 100644 --- a/config/locales/telephony/en.yml +++ b/config/locales/telephony/en.yml @@ -39,11 +39,11 @@ en: invalid_phone_number: The phone number entered is not valid. opt_out: The phone number entered has opted out of text messages. permanent_failure: The phone number entered is not valid. + rate_limited: That number is experiencing high message volume. Please try again + later. sms_unsupported: The phone number entered doesn’t support text messaging. Try the Phone call option. temporary_failure: We are experiencing technical difficulties. Please try again later. - throttled: That number is experiencing high message volume. Please try again - later. timeout: The server took too long to respond. Please try again. unknown_failure: We are experiencing technical difficulties. Please try again later. voice_unsupported: Invalid phone number. Check that you’ve entered the correct diff --git a/config/locales/telephony/es.yml b/config/locales/telephony/es.yml index 4db9376411d..682909ff6ec 100644 --- a/config/locales/telephony/es.yml +++ b/config/locales/telephony/es.yml @@ -40,12 +40,12 @@ es: opt_out: El número de teléfono ingresado ha sido excluido de los mensajes de texto. permanent_failure: El número de teléfono ingresado no es válido. + rate_limited: Ese número está experimentando un alto volumen de mensajes. Por + favor, inténtelo de nuevo más tarde. sms_unsupported: El número de teléfono ingresado no admite mensajes de texto. Pruebe la opción de llamada telefónica. temporary_failure: Estamos experimentando dificultades técnicas. Por favor, inténtelo de nuevo más tarde. - throttled: Ese número está experimentando un alto volumen de mensajes. Por - favor, inténtelo de nuevo más tarde. timeout: El servidor tardó demasiado en responder. Inténtalo de nuevo. unknown_failure: Estamos experimentando dificultades técnicas. Por favor, inténtelo de nuevo más tarde. diff --git a/config/locales/telephony/fr.yml b/config/locales/telephony/fr.yml index 5d9b8d670b8..41a35bbf284 100644 --- a/config/locales/telephony/fr.yml +++ b/config/locales/telephony/fr.yml @@ -41,12 +41,12 @@ fr: invalid_phone_number: Le numéro de téléphone saisi n’est pas valide. opt_out: Le numéro de téléphone entré a désactivé les messages texte. permanent_failure: Le numéro de téléphone entré n’est pas valide. + rate_limited: Ce nombre connaît un volume de messages élevé. Veuillez réessayer + plus tard. sms_unsupported: Le numéro de téléphone saisi ne prend pas en charge les messages textuels. Veuillez essayer l’option d’appel téléphonique. temporary_failure: Nous rencontrons des difficultés techniques. Veuillez réessayer plus tard. - throttled: Ce nombre connaît un volume de messages élevé. Veuillez réessayer - plus tard. timeout: Le serveur a pris trop de temps pour répondre. Veuillez réessayer. unknown_failure: Nous rencontrons des difficultés techniques. Veuillez réessayer plus tard. diff --git a/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb b/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb new file mode 100644 index 00000000000..a92e876c7f9 --- /dev/null +++ b/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb @@ -0,0 +1,5 @@ +class RemoveReproofAtFromProfiles < ActiveRecord::Migration[7.0] + def change + safety_assured { remove_column :profiles, :reproof_at, :date } + end +end diff --git a/db/primary_migrate/20230720183509_create_suspended_emails_table.rb b/db/primary_migrate/20230720183509_create_suspended_emails_table.rb new file mode 100644 index 00000000000..494cce27e04 --- /dev/null +++ b/db/primary_migrate/20230720183509_create_suspended_emails_table.rb @@ -0,0 +1,9 @@ +class CreateSuspendedEmailsTable < ActiveRecord::Migration[7.0] + def change + create_table :suspended_emails do |t| + t.references :email_address, null: false + t.string :digested_base_email, null: false, index: true + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index bcf0914a68b..cae30ecad3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_07_07_144310) do +ActiveRecord::Schema[7.0].define(version: 2023_07_20_183509) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pgcrypto" @@ -441,7 +441,6 @@ t.integer "deactivation_reason" t.jsonb "proofing_components" t.string "name_zip_birth_year_signature" - t.date "reproof_at" t.string "initiating_service_provider_issuer" t.datetime "fraud_review_pending_at" t.datetime "fraud_rejection_at" @@ -452,7 +451,6 @@ t.index ["fraud_review_pending_at"], name: "index_profiles_on_fraud_review_pending_at" t.index ["gpo_verification_pending_at"], name: "index_profiles_on_gpo_verification_pending_at" t.index ["name_zip_birth_year_signature"], name: "index_profiles_on_name_zip_birth_year_signature" - t.index ["reproof_at"], name: "index_profiles_on_reproof_at" t.index ["ssn_signature"], name: "index_profiles_on_ssn_signature" t.index ["user_id", "active"], name: "index_profiles_on_user_id_and_active", unique: true, where: "(active = true)" t.index ["user_id"], name: "index_profiles_on_user_id" @@ -572,6 +570,15 @@ t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true end + create_table "suspended_emails", force: :cascade do |t| + t.bigint "email_address_id", null: false + t.string "digested_base_email", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["digested_base_email"], name: "index_suspended_emails_on_digested_base_email" + t.index ["email_address_id"], name: "index_suspended_emails_on_email_address_id" + end + create_table "users", id: :serial, force: :cascade do |t| t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at", precision: nil diff --git a/lib/app_artifacts.rb b/lib/app_artifacts.rb index 9a3d33c797e..82160a429dd 100644 --- a/lib/app_artifacts.rb +++ b/lib/app_artifacts.rb @@ -36,7 +36,7 @@ def add_artifact(name, path) private def read_artifact(path) - if Identity::Hostdata.in_datacenter? + if Identity::Hostdata.in_datacenter? && !ENV['LOGIN_SKIP_REMOTE_CONFIG'] secrets_s3.read_file(path) else read_local_artifact(path) diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 9119edc93f1..1c2368a2368 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -99,8 +99,6 @@ def self.build_store(config_map) config.add(:account_reset_token_valid_for_days, type: :integer) config.add(:account_reset_wait_period_days, type: :integer) config.add(:account_suspended_support_code, type: :string) - config.add(:acuant_maintenance_window_start, type: :timestamp, allow_nil: true) - config.add(:acuant_maintenance_window_finish, type: :timestamp, allow_nil: true) config.add(:acuant_assure_id_password) config.add(:acuant_assure_id_username) config.add(:acuant_assure_id_subscription_id) diff --git a/lib/tasks/remove_verified_at_for_non_verified.rake b/lib/tasks/remove_verified_at_for_non_verified.rake new file mode 100644 index 00000000000..ee6a0d1c598 --- /dev/null +++ b/lib/tasks/remove_verified_at_for_non_verified.rake @@ -0,0 +1,68 @@ +namespace :profiles do + desc 'Remove verified_at if a profile is gpo, fraud pending or fraud rejected' + + ## + # Usage: + # + # Print errant profiles + # bundle exec rake profiles:remove_verified_at_from_non_verified_profiles + # + # Commit updates + # bundle exec rake profiles:remove_verified_at_from_non_verified_profiles UPDATE_PROFILES=true + # + task remove_verified_at_from_non_verified_profiles: :environment do |_task, _args| + ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') + + update_profiles = ENV['UPDATE_PROFILES'] == 'true' + + profiles = Profile.where('verified_at IS NOT NULL'). + where('fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL OR + gpo_verification_pending_at IS NOT NULL') + + profiles.each do |profile| + warn "#{profile.id},#{profile.verified_at}, #{profile.user.uuid}" + profile.update!(verified_at: nil) if update_profiles + end + end + + ## + # Usage: + # + # Rollback the above: + # + # export BACKFILL_OUTPUT='' + # bundle exec rake profiles:rollback_remove_verified_at_from_non_verified_profiles + # + task rollback_remove_verified_at_from_non_verified_profiles: :environment do |_task, _args| + ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') + + profiles = ENV['VERIFIED_OUTPUT'] + + warn "Updating #{profiles.count} records" + + profiles.split("\n").map do |profile_row| + profile_id, profile_verified_at = profile_row.split(',', 2) + + Profile.find(profile_id).update(verified_at: profile_verified_at) + end + end + + ## + # Usage: + # bundle exec rake profiles:validate_remove_verified_at_from_non_verified_profiles + # + task validate_remove_verified_at_from_non_verified_profiles: :environment do |_task, _args| + ActiveRecord::Base.connection.execute('SET statement_timeout = 60000') + + profiles = Profile.where( + verified_at: nil, + ).where('fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL OR + gpo_verification_pending_at IS NOT NULL') + + if profiles.empty? + warn 'remove verified_at from profiles that were not verified was successful' + else + warn "remove verified_at from profiles that were not verified left #{profiles.count} rows" + end + end +end diff --git a/lib/telephony/errors.rb b/lib/telephony/errors.rb index e301b692cd6..62453eec2f9 100644 --- a/lib/telephony/errors.rb +++ b/lib/telephony/errors.rb @@ -68,10 +68,10 @@ def friendly_error_message_key end end - class ThrottledError < TelephonyError + class RateLimitedError < TelephonyError def friendly_error_message_key - # i18n-tasks-use t('telephony.error.friendly_message.throttled') - 'telephony.error.friendly_message.throttled' + # i18n-tasks-use t('telephony.error.friendly_message.rate_limited') + 'telephony.error.friendly_message.rate_limited' end end diff --git a/lib/telephony/pinpoint/sms_sender.rb b/lib/telephony/pinpoint/sms_sender.rb index ff6778af320..69fccf8e5e3 100644 --- a/lib/telephony/pinpoint/sms_sender.rb +++ b/lib/telephony/pinpoint/sms_sender.rb @@ -8,7 +8,7 @@ class SmsSender 'OPT_OUT' => OptOutError, 'PERMANENT_FAILURE' => PermanentFailureError, 'TEMPORARY_FAILURE' => TemporaryFailureError, - 'THROTTLED' => ThrottledError, + 'THROTTLED' => RateLimitedError, 'TIMEOUT' => TimeoutError, 'UNKNOWN_FAILURE' => UnknownFailureError, }.freeze diff --git a/lib/telephony/pinpoint/voice_sender.rb b/lib/telephony/pinpoint/voice_sender.rb index 3013df34146..7696109d4f4 100644 --- a/lib/telephony/pinpoint/voice_sender.rb +++ b/lib/telephony/pinpoint/voice_sender.rb @@ -81,7 +81,7 @@ def handle_pinpoint_error(err) error_message = "#{err.class}: #{err.message}" error_class = if err.is_a? Aws::PinpointSMSVoice::Errors::LimitExceededException - Telephony::ThrottledError + Telephony::RateLimitedError elsif err.is_a? Aws::PinpointSMSVoice::Errors::TooManyRequestsException Telephony::DailyLimitReachedError else diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb index 766ddfd71e5..2e9f99d9931 100644 --- a/spec/components/webauthn_input_component_spec.rb +++ b/spec/components/webauthn_input_component_spec.rb @@ -6,12 +6,7 @@ subject(:rendered) { render_inline component } it 'renders element with expected attributes' do - element = rendered.css('lg-webauthn-input').first - - expect(element.attr('hidden')).to be_present - expect(element.attr('platform')).to be_nil - expect(element.attr('passkey-supported-only')).to be_nil - expect(element.attr('show-unsupported-passkey')).to be_nil + expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])') end it 'exposes boolean alias for platform option' do @@ -28,66 +23,63 @@ context 'with platform option' do context 'with platform option false' do - let(:options) { { platform: false } } + let(:options) { super().merge(platform: false) } - it 'renders without platform attribute' do - expect(rendered).to have_css('lg-webauthn-input[hidden]:not([platform])', visible: false) + it 'renders as visible for js-enabled browsers' do + expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])') end end context 'with platform option true' do - let(:options) { { platform: true } } + let(:options) { super().merge(platform: true) } - it 'renders with platform attribute' do - expect(rendered).to have_css('lg-webauthn-input[hidden][platform]', visible: false) + it 'renders as visible for js-enabled browsers' do + expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])') end - end - end - - context 'with passkey_supported_only option' do - context 'with passkey_supported_only option false' do - let(:options) { { passkey_supported_only: false } } - - it 'renders without passkey-supported-only attribute' do - expect(rendered).to have_css( - 'lg-webauthn-input[hidden]:not([passkey-supported-only])', - visible: false, - ) - end - end - - context 'with passkey_supported_only option true' do - let(:options) { { passkey_supported_only: true } } - - it 'renders with passkey-supported-only attribute' do - expect(rendered).to have_css( - 'lg-webauthn-input[hidden][passkey-supported-only]', - visible: false, - ) - end - end - end - - context 'with show_unsupported_passkey option' do - context 'with show_unsupported_passkey option false' do - let(:options) { { show_unsupported_passkey: false } } - - it 'renders without show-unsupported-passkey attribute' do - expect(rendered).to have_css( - 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])', - visible: false, - ) - end - end - - context 'with show_unsupported_passkey option true' do - let(:options) { { show_unsupported_passkey: true } } - it 'renders with show-unsupported-passkey attribute' do - expect(rendered).to have_css( - 'lg-webauthn-input[hidden][show-unsupported-passkey]', - visible: false, - ) + context 'with passkey_supported_only option' do + context 'with passkey_supported_only option false' do + let(:options) { super().merge(passkey_supported_only: false) } + + it 'renders as visible for js-enabled browsers' do + expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])') + end + end + + context 'with passkey_supported_only option true' do + let(:options) { super().merge(passkey_supported_only: true) } + + it 'renders as hidden' do + expect(rendered).to have_css( + 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])', + visible: false, + ) + end + + context 'with show_unsupported_passkey option' do + context 'with show_unsupported_passkey option false' do + let(:options) { super().merge(show_unsupported_passkey: false) } + + it 'renders as hidden' do + expect(rendered).to have_css( + 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])', + visible: false, + ) + end + end + + context 'with show_unsupported_passkey option true' do + let(:options) { super().merge(show_unsupported_passkey: true) } + + it 'renders with show-unsupported-passkey attribute' do + expect(rendered).to have_css( + 'lg-webauthn-input[hidden][show-unsupported-passkey]', + visible: false, + ) + end + end + end + end end end end @@ -96,7 +88,7 @@ let(:options) { super().merge(data: { foo: 'bar' }) } it 'renders with additional attributes' do - expect(rendered).to have_css('lg-webauthn-input[hidden][data-foo="bar"]', visible: false) + expect(rendered).to have_css('lg-webauthn-input[data-foo="bar"]') end end end diff --git a/spec/controllers/idv/gpo_verify_controller_spec.rb b/spec/controllers/idv/gpo_verify_controller_spec.rb index c4720e654c7..2736311dd38 100644 --- a/spec/controllers/idv/gpo_verify_controller_spec.rb +++ b/spec/controllers/idv/gpo_verify_controller_spec.rb @@ -131,7 +131,7 @@ success: true, errors: {}, pending_in_person_enrollment: false, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) @@ -178,7 +178,7 @@ success: true, errors: {}, pending_in_person_enrollment: true, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) @@ -203,7 +203,6 @@ create( :profile, :with_pii, - :fraud_review_pending, fraud_pending_reason: 'threatmetrix_reject', user: user, ) @@ -215,7 +214,7 @@ success: true, errors: {}, pending_in_person_enrollment: false, - threatmetrix_check_failed: true, + fraud_check_failed: true, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) @@ -240,7 +239,6 @@ create( :profile, :with_pii, - :fraud_review_pending, fraud_pending_reason: 'threatmetrix_reject', user: user, ) @@ -252,7 +250,7 @@ success: true, errors: {}, pending_in_person_enrollment: false, - threatmetrix_check_failed: true, + fraud_check_failed: true, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) @@ -278,7 +276,6 @@ create( :profile, :with_pii, - :fraud_review_pending, fraud_pending_reason: 'threatmetrix_review', user: user, ) @@ -290,7 +287,7 @@ success: true, errors: {}, pending_in_person_enrollment: false, - threatmetrix_check_failed: true, + fraud_check_failed: true, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ) @@ -312,7 +309,7 @@ success: false, errors: otp_code_error_message, pending_in_person_enrollment: false, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: nil, error_details: otp_code_incorrect, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], @@ -343,7 +340,7 @@ success: false, errors: otp_code_error_message, pending_in_person_enrollment: false, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: nil, error_details: otp_code_incorrect, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], @@ -387,7 +384,7 @@ success: false, errors: otp_code_error_message, pending_in_person_enrollment: false, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: nil, error_details: otp_code_incorrect, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], @@ -397,7 +394,7 @@ success: true, errors: {}, pending_in_person_enrollment: false, - threatmetrix_check_failed: false, + fraud_check_failed: false, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], ).once diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 153b177cca9..08def1c0bf0 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -249,10 +249,10 @@ 'IdV: doc auth image upload form submitted', success: false, errors: { - limit: [I18n.t('errors.doc_auth.throttled_heading')], + limit: [I18n.t('errors.doc_auth.rate_limited_heading')], }, error_details: { - limit: [I18n.t('errors.doc_auth.throttled_heading')], + limit: [I18n.t('errors.doc_auth.rate_limited_heading')], }, user_id: user.uuid, attempts: IdentityConfig.store.doc_auth_max_attempts, diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index 7ef46e95e25..c393733e1ca 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -84,61 +84,167 @@ expect(response).to redirect_to idv_in_person_step_url(step: :address) end end + end + end + + describe '#show' do + context 'when in_person_ssn_info_controller_enabled is true' do + before do + allow(IdentityConfig.store).to receive(:in_person_ssn_info_controller_enabled). + and_return(true) + end + let(:analytics_name) { 'IdV: doc auth ssn visited' } + let(:analytics_args) do + { + analytics_id: 'In Person Proofing', + flow_path: 'standard', + irs_reproofing: false, + step: 'ssn', + }.merge(ab_test_args) + end + + it 'renders the show template' do + get :show + + expect(response).to render_template :show + end + + it 'sends analytics_visited event' do + get :show + + expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args) + end + + it 'updates DocAuthLog ssn_view_count' do + doc_auth_log = DocAuthLog.create(user_id: user.id) + + expect { get :show }.to( + change { doc_auth_log.reload.ssn_view_count }.from(0).to(1), + ) + end + + context 'with an ssn in session' do + let(:referer) { idv_document_capture_url } + before do + flow_session['pii_from_user'][:ssn] = ssn + request.env['HTTP_REFERER'] = referer + end + + context 'referer is not verify_info' do + it 'redirects to verify_info' do + get :show + + expect(response).to redirect_to(idv_in_person_verify_info_url) + end + end + + context 'referer is verify_info' do + let(:referer) { idv_in_person_verify_info_url } + it 'does not redirect' do + get :show + + expect(response).to render_template :show + end + end + end + end + end + + describe '#update' do + context 'when in_person_ssn_info_controller_enabled is true' do + before do + allow(IdentityConfig.store).to receive(:in_person_ssn_info_controller_enabled). + and_return(true) + end - describe '#show' do - let(:analytics_name) { 'IdV: doc auth ssn visited' } + context 'valid ssn' do + let(:params) { { doc_auth: { ssn: ssn } } } + let(:analytics_name) { 'IdV: doc auth ssn submitted' } let(:analytics_args) do { analytics_id: 'In Person Proofing', flow_path: 'standard', irs_reproofing: false, step: 'ssn', + success: true, + errors: {}, + pii_like_keypaths: [[:errors, :ssn], [:error_details, :ssn]], }.merge(ab_test_args) end - it 'renders the show template' do - get :show - - expect(response).to render_template :show + let(:idv_session) do + { + applicant: Idp::Constants::MOCK_IDV_APPLICANT, + resolution_successful: true, + profile_confirmation: true, + vendor_phone_confirmation: true, + user_phone_confirmation: true, + } end - it 'sends analytics_visited event' do - get :show + it 'sends analytics_submitted event' do + put :update, params: params expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args) end - it 'updates DocAuthLog ssn_view_count' do - doc_auth_log = DocAuthLog.create(user_id: user.id) - - expect { get :show }.to( - change { doc_auth_log.reload.ssn_view_count }.from(0).to(1), + it 'logs attempts api event' do + expect(@irs_attempts_api_tracker).to receive(:idv_ssn_submitted).with( + ssn: ssn, ) + + put :update, params: params end - context 'with an ssn in session' do - let(:referer) { idv_document_capture_url } - before do - flow_session['pii_from_user'][:ssn] = ssn - request.env['HTTP_REFERER'] = referer - end + it 'merges ssn into pii session value' do + put :update, params: params - context 'referer is not verify_info' do - it 'redirects to verify_info' do - get :show + expect(flow_session['pii_from_user'][:ssn]).to eq(ssn) + end - expect(response).to redirect_to(idv_in_person_verify_info_url) - end - end + it 'invalidates steps after ssn' do + put :update, params: params - context 'referer is verify_info' do - let(:referer) { idv_in_person_verify_info_url } - it 'does not redirect' do - get :show + expect(subject.idv_session.applicant).to be_blank + expect(subject.idv_session.resolution_successful).to be_blank + expect(subject.idv_session.profile_confirmation).to be_blank + expect(subject.idv_session.vendor_phone_confirmation).to be_blank + expect(subject.idv_session.user_phone_confirmation).to be_blank + end - expect(response).to render_template :show - end - end + it 'redirects to the expected page' do + put :update, params: params + + expect(response).to redirect_to idv_in_person_verify_info_url + end + end + + context 'invalid ssn' do + let(:params) { { doc_auth: { ssn: 'i am not an ssn' } } } + let(:analytics_name) { 'IdV: doc auth ssn submitted' } + let(:analytics_args) do + { + analytics_id: 'In Person Proofing', + flow_path: 'standard', + irs_reproofing: false, + step: 'ssn', + success: false, + errors: { + ssn: ['Enter a nine-digit Social Security number'], + }, + error_details: { ssn: [:invalid] }, + pii_like_keypaths: [[:errors, :ssn], [:error_details, :ssn]], + }.merge(ab_test_args) + end + + render_views + + it 'renders the show template with an error message' do + put :update, params: params + + expect(response).to have_rendered(:show) + expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args) + expect(response.body).to include('Enter a nine-digit Social Security number') end end end diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index 274909c3cc7..18ff8310a9a 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -11,6 +11,7 @@ let(:normalized_phone) { '7035550000' } let(:bad_phone) { '+1 (703) 555-5555' } let(:international_phone) { '+81 54 354 3643' } + let(:timeout_phone) { '7035555888' } describe 'before_actions' do it 'includes authentication before_action' do @@ -328,6 +329,7 @@ ) expect(subject.idv_session.vendor_phone_confirmation).to eq true expect(subject.idv_session.user_phone_confirmation).to eq false + expect(subject.idv_session.failed_phone_step_numbers).to be_empty end context 'with full vendor outage' do @@ -440,6 +442,22 @@ expect(subject.idv_session.vendor_phone_confirmation).to be_falsy expect(subject.idv_session.user_phone_confirmation).to be_falsy + expect(subject.idv_session.failed_phone_step_numbers).to contain_exactly('+17035555555') + end + + it 'renders timeout page and does not set phone confirmation' do + user = build(:user, with: { phone: '+1 (415) 555-0130', phone_confirmed_at: Time.zone.now }) + stub_verify_steps_one_and_two(user) + + put :create, params: { idv_phone_form: { phone: timeout_phone } } + + expect(response).to redirect_to idv_phone_path + get :new + expect(response).to redirect_to idv_phone_errors_timeout_path + + expect(subject.idv_session.vendor_phone_confirmation).to be_falsy + expect(subject.idv_session.user_phone_confirmation).to be_falsy + expect(subject.idv_session.failed_phone_step_numbers).to be_empty end it 'tracks event with invalid phone' do diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index 0cdfdf9ae82..a1dc9aef164 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -341,7 +341,6 @@ issuer: 'foo', request_url: 'http://example.com', } - expect(@irs_attempts_api_tracker).not_to receive(:idv_reproof) patch :update end @@ -372,7 +371,6 @@ expect(additional_profile.initiating_service_provider).to be_nil expect(additional_profile.verified_at).to be_present - expect(@irs_attempts_api_tracker).to receive(:idv_reproof) patch :update end @@ -383,7 +381,6 @@ ial2: false, request_url: 'http://example.com', } - expect(@irs_attempts_api_tracker).not_to receive(:idv_reproof) patch :update diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index cca9e053abd..f4b0ef01117 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -235,6 +235,8 @@ error_details: reset_password_error_details, user_id: user.uuid, profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', } expect(@analytics).to have_received(:track_event). @@ -266,6 +268,8 @@ error_details: password_short_error, user_id: user.uuid, profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', } expect(@analytics).to receive(:track_event). @@ -341,6 +345,8 @@ errors: {}, user_id: user.uuid, profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', } expect(@analytics).to have_received(:track_event). @@ -389,6 +395,8 @@ errors: {}, user_id: user.uuid, profile_deactivated: true, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', } expect(@analytics).to have_received(:track_event). @@ -434,6 +442,8 @@ errors: {}, user_id: user.uuid, profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', } expect(@analytics).to have_received(:track_event). diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index bd18c1d5e20..4c0fe5032cb 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -623,7 +623,7 @@ def index expect(flash[:error]).to eq( I18n.t( - 'errors.messages.phone_confirmation_throttled', + 'errors.messages.phone_confirmation_limited', timeout: timeout, ), ) @@ -676,7 +676,7 @@ def index expect(flash[:error]).to eq( I18n.t( - 'errors.messages.phone_confirmation_throttled', + 'errors.messages.phone_confirmation_limited', timeout: timeout, ), ) diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index 67e7a38f842..7e0b5177bde 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -37,6 +37,11 @@ deactivation_reason { :in_person_verification_pending } end + trait :fraud_pending_reason do + fraud_pending_reason { 'threatmetrix_review' } + proofing_components { { threatmetrix_review_status: 'review' } } + end + trait :fraud_review_pending do fraud_pending_reason { 'threatmetrix_review' } fraud_review_pending_at { 15.days.ago } diff --git a/spec/factories/suspended_emails.rb b/spec/factories/suspended_emails.rb new file mode 100644 index 00000000000..a018f738036 --- /dev/null +++ b/spec/factories/suspended_emails.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :suspended_email do + digested_base_email { 'test_digest' } + association :email_address + end +end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 7ee0c172226..203b39d2333 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -50,7 +50,7 @@ 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, - 'IdV: phone of record visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, + 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' }, otp_delivery_preference: 'sms' }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, area_code: '202', country_code: 'US', phone_fingerprint: anything }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything }, @@ -85,7 +85,7 @@ 'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', double_address_verification: false, should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, - 'IdV: phone of record visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, + 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: USPS address letter requested' => { resend: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 8efe4181a4d..906d3efcebe 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -67,7 +67,7 @@ timeout = distance_of_time_in_words( RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes, ) - message = strip_tags(t('errors.doc_auth.throttled_text_html', timeout: timeout)) + message = strip_tags(t('errors.doc_auth.rate_limited_text_html', timeout: timeout)) expect(page).to have_content(message) expect(page).to have_current_path(idv_session_errors_throttled_path) end diff --git a/spec/features/idv/doc_auth/getting_started_spec.rb b/spec/features/idv/doc_auth/getting_started_spec.rb index 661ce15beeb..0d3ad5a7e16 100644 --- a/spec/features/idv/doc_auth/getting_started_spec.rb +++ b/spec/features/idv/doc_auth/getting_started_spec.rb @@ -5,7 +5,6 @@ include DocAuthHelper let(:fake_analytics) { FakeAnalytics.new } - let(:maintenance_window) { [] } let(:sp_name) { 'Test SP' } before do diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 302dd710bde..cca341a2c8e 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -144,7 +144,7 @@ freeze_time do idv_send_link_max_attempts.times do expect(page).to_not have_content( - I18n.t('errors.doc_auth.send_link_throttle', timeout: timeout), + I18n.t('errors.doc_auth.send_link_limited', timeout: timeout), ) fill_in :doc_auth_phone, with: '415-555-0199' @@ -161,7 +161,7 @@ expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) expect(page).to have_content( I18n.t( - 'errors.doc_auth.send_link_throttle', + 'errors.doc_auth.send_link_limited', timeout: timeout, ), ) diff --git a/spec/features/idv/doc_auth/welcome_spec.rb b/spec/features/idv/doc_auth/welcome_spec.rb index d1b77ff1625..42334e19a77 100644 --- a/spec/features/idv/doc_auth/welcome_spec.rb +++ b/spec/features/idv/doc_auth/welcome_spec.rb @@ -5,15 +5,11 @@ include DocAuthHelper let(:fake_analytics) { FakeAnalytics.new } - let(:maintenance_window) { [] } let(:sp_name) { 'Test SP' } before do allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) allow_any_instance_of(ServiceProviderSessionDecorator).to receive(:sp_name).and_return(sp_name) - start, finish = maintenance_window - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start) - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish) visit_idp_from_sp_with_ial2(:oidc) sign_in_and_2fa_user @@ -80,20 +76,4 @@ ), ) end - - context 'during the acuant maintenance window' do - let(:maintenance_window) do - [Time.zone.parse('2020-01-01T00:00:00Z'), Time.zone.parse('2020-01-01T23:59:59Z')] - end - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - - around do |ex| - travel_to(now) { ex.run } - end - - it 'renders the warning banner but no other content' do - expect(page).to have_content('We are currently under maintenance') - expect(page).to_not have_content(t('doc_auth.headings.welcome')) - end - end end diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 73e1d7dd20f..c6f0f633a02 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -165,6 +165,10 @@ def validate_phone_page t('two_factor_authentication.otp_delivery_preference.sms'), visible: false ) + # displays phone number by default + phone_field = find_field(t('two_factor_authentication.phone_label')) + expect(phone_field.value).not_to be_empty + # displays error if invalid phone number is entered fill_in :idv_phone_form_phone, with: '578190' click_idv_send_security_code diff --git a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb index 6ba031d7320..4fdf2b4185f 100644 --- a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb +++ b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb @@ -18,9 +18,7 @@ ssn: '123-45-6789', dob: '1970-01-01', }, - fraud_review_pending_at: fraud_review_pending_timestamp, fraud_pending_reason: fraud_pending_reason, - fraud_rejection_at: fraud_rejection_timestamp, ) end let(:gpo_confirmation_code) do @@ -32,9 +30,7 @@ end let(:user) { profile.user } let(:threatmetrix_enabled) { false } - let(:fraud_review_pending_timestamp) { nil } let(:fraud_pending_reason) { nil } - let(:fraud_rejection_timestamp) { nil } let(:redirect_after_verification) { nil } let(:profile_should_be_active) { true } let(:fraud_review_pending) { false } @@ -48,7 +44,6 @@ context 'ThreatMetrix disabled, but we have ThreatMetrix status on proofing component' do let(:threatmetrix_enabled) { false } - let(:fraud_review_pending_timestamp) { 1.day.ago } let(:fraud_pending_reason) { 'threatmetrix_review' } it_behaves_like 'gpo otp verification' end @@ -57,12 +52,10 @@ let(:threatmetrix_enabled) { true } context 'ThreatMetrix says "pass"' do - let(:fraud_review_pending_timestamp) { nil } it_behaves_like 'gpo otp verification' end context 'ThreatMetrix says "review"' do - let(:fraud_review_pending_timestamp) { 1.day.ago } let(:fraud_pending_reason) { 'threatmetrix_review' } let(:profile_should_be_active) { false } let(:fraud_review_pending) { true } @@ -70,15 +63,14 @@ end context 'ThreatMetrix says "reject"' do - let(:fraud_rejection_timestamp) { 1.day.ago } - let(:fraud_pending_reason) { 'threatmetrix_review' } + let(:fraud_pending_reason) { 'threatmetrix_reject' } let(:profile_should_be_active) { false } let(:fraud_review_pending) { true } it_behaves_like 'gpo otp verification' end context 'No ThreatMetrix result on proofing component' do - let(:fraud_review_pending_timestamp) { nil } + let(:fraud_pending_reason) { nil } it_behaves_like 'gpo otp verification' end end diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 0127ab3f154..924ebd6b120 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -76,16 +76,125 @@ end context "when the user's information cannot be verified" do - it 'reports the number the user entered' do + before do start_idv_from_sp complete_idv_steps_before_phone_step fill_out_phone_form_fail + end + + it 'reports the number the user entered' do click_idv_send_security_code expect(page).to have_content(t('idv.failure.phone.warning.heading')) expect(page).to have_content('+1 703-555-5555') end + context 'resubmission after number failed verification' do + it 'phone field is empty after invalid submission' do + phone_field = find_field(t('two_factor_authentication.phone_label')) + + expect(phone_field.value).not_to be_empty + + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + expect(phone_field.value).to be_empty + end + + it 'succeeds to otp verification with valid number resubmission' do + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + + fill_out_phone_form_ok + click_idv_send_security_code + expect(page).to have_current_path(idv_otp_verification_path) + end + + context 'displays alert message if same nubmer is resubmitted' do + context 'gpo verification is enabled' do + it 'includes verify link' do + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + + fill_out_phone_form_fail + + expect(page).to have_content(t('idv.messages.phone.failed_number.alert_text')) + + expect(page).to have_content( + strip_tags( + t( + 'idv.messages.phone.failed_number.gpo_alert_html', + link_html: t('idv.messages.phone.failed_number.gpo_verify_link'), + ), + ), + ) + + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + end + end + + context 'gpo verification is disabled' do + before do + allow(IdentityConfig.store).to receive(:enable_usps_verification).and_return(false) + end + + it 'does not display verify link' do + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + + fill_out_phone_form_fail + + expect(page).to have_content(t('idv.messages.phone.failed_number.alert_text')) + expect(page).not_to have_content( + strip_tags( + t( + 'idv.messages.phone.failed_number.gpo_alert_html', + link_html: t('idv.messages.phone.failed_number.gpo_verify_link'), + ), + ), + ) + + click_idv_send_security_code + click_on t('idv.failure.phone.warning.try_again_button') + + expect(page).to have_current_path(idv_phone_path) + end + end + end + end + + context 'phone number submission times out' do + it 'does not display failed alert message' do + timeout_phone_number = '7035555888' + start_idv_from_sp + complete_idv_steps_before_phone_step + fill_out_phone_form_ok(timeout_phone_number) + click_idv_send_security_code + click_on t('idv.failure.button.warning') + + expect(page).to have_current_path(idv_phone_path) + + fill_out_phone_form_ok(timeout_phone_number) + + expect(page).not_to have_content(t('idv.messages.phone.failed_number.alert_text')) + + click_idv_send_security_code + click_on t('idv.failure.button.warning') + + expect(page).to have_current_path(idv_phone_path) + end + end + it 'goes to the cancel page when cancel link is clicked' do start_idv_from_sp complete_idv_steps_before_phone_step @@ -161,6 +270,7 @@ click_idv_continue_for_step(:phone) click_on t('idv.failure.phone.warning.try_again_button') end + fill_out_phone_form_fail click_idv_continue_for_step(:phone) end diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb index 7584acf90f3..c2905b8c7d7 100644 --- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb @@ -75,6 +75,51 @@ expect(current_path).to eq account_path end + scenario 'user can select 2 MFA methods and complete after reauthn window' do + allow(IdentityConfig.store).to receive(:reauthn_window).and_return(10) + sign_in_before_2fa + + expect(current_path).to eq authentication_methods_setup_path + + click_2fa_option('backup_code') + click_2fa_option('auth_app') + + click_continue + + expect(current_path).to eq authenticator_setup_path + fill_in t('forms.totp_setup.totp_step_1'), with: 'App' + + secret = find('#qr-code').text + totp = generate_totp_code(secret) + + fill_in :code, with: totp + check t('forms.messages.remember_device') + click_submit_default + + expect(current_path).to eq backup_code_setup_path + travel_to((IdentityConfig.store.reauthn_window + 5).seconds.from_now) do + click_continue + expect(current_path).to eq login_two_factor_options_path + + find("label[for='two_factor_options_form_selection_auth_app']").click + click_on t('forms.buttons.continue') + + totp = generate_totp_code(secret) + fill_in :code, with: totp + + click_submit_default + click_continue + + expect(page).to have_link(t('components.download_button.label')) + + click_continue + + expect(page).to have_content(t('notices.backup_codes_configured')) + + expect(current_path).to eq account_path + end + end + scenario 'user can select 1 MFA methods and will be prompted to add another method' do sign_in_before_2fa diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 1f3a0cfb054..cf4df12e992 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -123,7 +123,7 @@ # whether it says '9 minutes' or '10 minutes' depends on how # slowly the test runs. throttled_message = I18n.t( - 'errors.messages.phone_confirmation_throttled', + 'errors.messages.phone_confirmation_limited', timeout: '(10|9) minutes', ) diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 5ce217fb300..73045e44b40 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' RSpec.describe 'webauthn hide' do + include JavascriptDriverHelper + describe 'security key' do let(:option_id) { 'two_factor_options_form_selection_webauthn' } @@ -102,9 +104,11 @@ end def webauthn_option_hidden? - page.find("label[for=#{option_id}]") - false - rescue Capybara::ElementNotFound - true + label = page.find("label[for=#{option_id}]", visible: :all) + if javascript_enabled? + !label.visible? + else + label.ancestor('.js,[hidden]', visible: :all).present? + end end end diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index 4e9b69843e7..d6d54a9b23e 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -151,11 +151,9 @@ context 'ThreatMetrix rejection' do let(:pending_profile) do - create(:profile, :verify_by_mail_pending, :fraud_review_pending, user: user) + create(:profile, :verify_by_mail_pending, :fraud_pending_reason, user: user) end - let(:threatmetrix_review_status) { 'reject' } - before do allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end @@ -174,7 +172,7 @@ it 'notes that threatmetrix failed' do result = subject.submit - expect(result.extra).to include(threatmetrix_check_failed: true) + expect(result.extra).to include(fraud_check_failed: true) end context 'threatmetrix is not required for verification' do @@ -196,7 +194,7 @@ it 'notes that threatmetrix failed' do result = subject.submit - expect(result.extra).to include(threatmetrix_check_failed: true) + expect(result.extra).to include(fraud_check_failed: true) end end end diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index cc1bb11624b..d5048799bd5 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -68,7 +68,7 @@ form.submit expect(form.valid?).to eq(false) - expect(form.errors[:limit]).to eq([I18n.t('errors.doc_auth.throttled_heading')]) + expect(form.errors[:limit]).to eq([I18n.t('errors.doc_auth.rate_limited_heading')]) end end end diff --git a/spec/forms/register_user_email_form_spec.rb b/spec/forms/register_user_email_form_spec.rb index ddbbdca081c..8f25d7c68b6 100644 --- a/spec/forms/register_user_email_form_spec.rb +++ b/spec/forms/register_user_email_form_spec.rb @@ -8,7 +8,7 @@ it_behaves_like 'email validation' describe '#submit' do - let(:email_domain) { 'test.com' } + let(:email_domain) { 'gmail.com' } let(:registered_email_address) { 'taken@' + email_domain } let(:unregistered_email_address) { 'not_taken@' + email_domain } let(:registered_and_confirmed_user) do @@ -68,6 +68,32 @@ end end + context 'email submission with special characters' do + context 'mx record are gmail' do + shared_examples 'blocked email address' do |email_address| + it 'sends the email with error code' do + user = create(*registered_and_confirmed_user) + user.suspend! + + subject.submit(email: email_address, terms_accepted: '1') + + expect_delivered_email_count(1) + expect_delivered_email( + to: [registered_email_address], + subject: t('user_mailer.suspended_create_account.subject'), + ) + expect(subject.send(:blocked_email_address).user).to eq(user) + end + end + context 'when email contains a plus sign' do + it_behaves_like 'blocked email address', 'taken+1@gmail.com' + end + context 'when email contains a dot' do + it_behaves_like 'blocked email address', 'tak.en@gmail.com' + end + end + end + let(:variation_of_preexisting_email) { 'TAKEN@' + email_domain } context 'when email is already taken' do let!(:existing_user) { create(*registered_and_confirmed_user) } diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb index 22d078d25f6..45c16209764 100644 --- a/spec/forms/reset_password_form_spec.rb +++ b/spec/forms/reset_password_form_spec.rb @@ -60,7 +60,6 @@ form = ResetPasswordForm.new(user) password = 'valid password' - extra = { user_id: '123', profile_deactivated: false } user_updater = instance_double(UpdateUser) allow(UpdateUser).to receive(:new). with(user: user, attributes: { password: password }).and_return(user_updater) @@ -69,7 +68,10 @@ expect(form.submit(password: password).to_h).to eq( success: true, errors: {}, - **extra, + user_id: '123', + profile_deactivated: false, + pending_profile_invalidated: false, + pending_profile_pending_reasons: '', ) end end @@ -151,6 +153,39 @@ end end + context 'when the user has a pending profile' do + it 'includes that the profile was not deactivated in the form response' do + profile = create(:profile, :verify_by_mail_pending, :in_person_verification_pending) + user = profile.user + user.update(reset_password_sent_at: Time.zone.now) + + form = ResetPasswordForm.new(user) + + result = form.submit(password: 'a good and powerful password') + + expect(result.success?).to eq(true) + expect(result.extra[:pending_profile_invalidated]).to eq(true) + expect(result.extra[:pending_profile_pending_reasons]).to eq( + 'gpo_verification_pending,in_person_verification_pending', + ) + end + end + + context 'when the user does not have a pending profile' do + it 'includes that the profile was not deactivated in the form response' do + user = create(:user) + user.update(reset_password_sent_at: Time.zone.now) + + form = ResetPasswordForm.new(user) + + result = form.submit(password: 'a good and powerful password') + + expect(result.success?).to eq(true) + expect(result.extra[:pending_profile_invalidated]).to eq(false) + expect(result.extra[:pending_profile_pending_reasons]).to eq('') + end + end + context 'when the unconfirmed email address has been confirmed by another account' do it 'does not raise an error and is not successful' do user = create(:user, :unconfirmed) diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx index c2219499287..a40e3787314 100644 --- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx @@ -55,7 +55,7 @@ describe('document-capture/components/review-issues-step', () => { , ); - expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok(); + expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok(); expect(getByText('3 attempts', { selector: 'strong' })).to.be.ok(); expect(getByText('remaining')).to.be.ok(); expect(getByRole('button', { name: 'idv.failure.button.warning' })).to.be.ok(); @@ -89,7 +89,7 @@ describe('document-capture/components/review-issues-step', () => { , ); - expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok(); + expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok(); expect(getByText('3 attempts', { selector: 'strong' })).to.be.ok(); expect(getByText('remaining')).to.be.ok(); expect(getByRole('button', { name: 'idv.failure.button.try_online' })).to.be.ok(); @@ -131,7 +131,7 @@ describe('document-capture/components/review-issues-step', () => { , ); - expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok(); + expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok(); expect(getByText('One attempt remaining')).to.be.ok(); expect(getByText('An unknown error occurred')).to.be.ok(); expect(getByRole('button', { name: 'idv.failure.button.warning' })).to.be.ok(); diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 17b4ecfa25b..daf7c6fe90f 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -194,6 +194,8 @@ end before do + allow(IdentityConfig.store). + to(receive(:in_person_results_delay_in_hours).and_return(nil)) allow(Rails).to receive(:cache).and_return( ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), ) @@ -217,7 +219,7 @@ let!(:pending_enrollments) do [ create( - :in_person_enrollment, :pending, :with_notification_phone_configuration, + :in_person_enrollment, :pending, selected_location_details: { name: 'BALTIMORE' }, issuer: 'http://localhost:3000' ), @@ -482,18 +484,15 @@ it 'sends deadline passed email on response with expired status' do stub_request_expired_proofing_results - allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). - and_return(true) user = pending_enrollment.user expect(pending_enrollment.deadline_passed_sent).to be false - expect(pending_enrollment.notification_phone_configuration).not_to be_nil freeze_time do expect do job.perform(Time.zone.now) end.to have_enqueued_mail(UserMailer, :in_person_deadline_passed).with( params: { user: user, email_address: user.email_addresses.first }, args: [{ enrollment: pending_enrollment }], - ).on_queue(:default) + ).at(:no_wait).on_queue(:default) pending_enrollment.reload expect(pending_enrollment.deadline_passed_sent).to be true expect(job_analytics).to have_logged_event( @@ -505,8 +504,6 @@ wait_until: nil, job_name: 'GetUspsProofingResultsJob', ) - expect(pending_enrollment.notification_phone_configuration).to be_nil - expect(pending_enrollment.notification_sent_at).to be_nil end end @@ -613,14 +610,12 @@ to(receive(:in_person_results_delay_in_hours).and_return(0)) user = pending_enrollment.user - freeze_time do - expect do - job.perform(Time.zone.now) - end.to have_enqueued_mail(UserMailer, :in_person_verified).with( - params: { user: user, email_address: user.email_addresses.first }, - args: [{ enrollment: pending_enrollment }], - ).on_queue(:default) - end + expect do + job.perform(Time.zone.now) + end.to have_enqueued_mail(UserMailer, :in_person_verified).with( + params: { user: user, email_address: user.email_addresses.first }, + args: [{ enrollment: pending_enrollment }], + ).at(:no_wait).on_queue(:default) end end end @@ -639,13 +634,17 @@ request_passed_proofing_results_response, ) - it 'logs details about the success' do + it 'invokes the SendProofingNotificationJob and logs details about the success' do allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). and_return(true) - expect do - job.perform(Time.zone.now) - end.to have_enqueued_job(InPerson::SendProofingNotificationJob). - with(pending_enrollment.id).on_queue(:intentionally_delayed) + expected_wait_until = nil + freeze_time do + expected_wait_until = 1.hour.from_now + expect do + job.perform(Time.zone.now) + end.to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id).at(expected_wait_until).on_queue(:intentionally_delayed) + end expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(job_analytics).to have_logged_event( 'GetUspsProofingResultsJob: Enrollment status updated', @@ -661,7 +660,7 @@ enrollment_code: pending_enrollment.enrollment_code, service_provider: anything, timestamp: anything, - wait_until: nil, + wait_until: expected_wait_until, job_name: 'GetUspsProofingResultsJob', ) end @@ -682,7 +681,11 @@ ) it 'logs failure details' do - job.perform(Time.zone.now) + expected_wait_until = nil + freeze_time do + expected_wait_until = 1.hour.from_now + job.perform(Time.zone.now) + end expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(job_analytics).to have_logged_event( @@ -699,7 +702,7 @@ enrollment_code: pending_enrollment.enrollment_code, service_provider: anything, timestamp: anything, - wait_until: nil, + wait_until: expected_wait_until, job_name: 'GetUspsProofingResultsJob', ) end @@ -720,7 +723,11 @@ ) it 'logs fraud failure details' do - job.perform(Time.zone.now) + expected_wait_until = nil + freeze_time do + expected_wait_until = 1.hour.from_now + job.perform(Time.zone.now) + end expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(job_analytics).to have_logged_event( @@ -738,7 +745,7 @@ enrollment_code: pending_enrollment.enrollment_code, service_provider: anything, timestamp: anything, - wait_until: nil, + wait_until: expected_wait_until, job_name: 'GetUspsProofingResultsJob', ) end @@ -759,7 +766,11 @@ ) it 'logs a message about the unsupported ID' do - job.perform Time.zone.now + expected_wait_until = nil + freeze_time do + expected_wait_until = 1.hour.from_now + job.perform Time.zone.now + end expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(job_analytics).to have_logged_event( @@ -777,7 +788,7 @@ enrollment_code: pending_enrollment.enrollment_code, service_provider: anything, timestamp: anything, - wait_until: nil, + wait_until: expected_wait_until, job_name: 'GetUspsProofingResultsJob', ) end @@ -1138,10 +1149,12 @@ it 'logs a message about enrollment with secondary ID' do allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). and_return(true) - expect do - job.perform Time.zone.now - end.to have_enqueued_job(InPerson::SendProofingNotificationJob). - with(pending_enrollment.id).on_queue(:intentionally_delayed) + freeze_time do + expect do + job.perform Time.zone.now + end.to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id).at(1.hour.from_now).on_queue(:intentionally_delayed) + end expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(pending_enrollment.profile.active).to eq(false) expect(job_analytics).to have_logged_event( @@ -1154,6 +1167,87 @@ ) end end + + context 'sms notifications enabled' do + let(:pending_enrollment) do + create( + :in_person_enrollment, + :pending, + :with_notification_phone_configuration, + capture_secondary_id_enabled: true, + ) + end + + before do + allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled). + and_return(true) + end + + context 'enrollment is expired' do + it 'deletes the notification phone configuration without sending an sms' do + stub_request_expired_proofing_results + + expect(pending_enrollment.notification_phone_configuration).to_not be_nil + + job.perform(Time.zone.now) + + expect(pending_enrollment.reload.notification_phone_configuration).to be_nil + expect(pending_enrollment.notification_sent_at).to be_nil + end + end + + context 'enrollment has passed proofing' do + it 'invokes the SendProofingNotificationJob for the enrollment' do + stub_request_passed_proofing_results + + expect(pending_enrollment.notification_phone_configuration).to_not be_nil + expect(pending_enrollment.notification_sent_at).to be_nil + + expect { job.perform(Time.zone.now) }. + to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id) + end + end + + context 'enrollment has failed proofing' do + it 'invokes the SendProofingNotificationJob for the enrollment' do + stub_request_failed_proofing_results + + expect(pending_enrollment.notification_phone_configuration).to_not be_nil + expect(pending_enrollment.notification_sent_at).to be_nil + + expect { job.perform(Time.zone.now) }. + to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id) + end + end + + context 'enrollment has failed proofing due to unsupported secondary ID' do + it 'invokes the SendProofingNotificationJob for the enrollment' do + stub_request_passed_proofing_secondary_id_type_results + + expect(pending_enrollment.notification_phone_configuration).to_not be_nil + expect(pending_enrollment.notification_sent_at).to be_nil + + expect { job.perform(Time.zone.now) }. + to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id) + end + end + + context 'enrollment has failed proofing due to unsupported ID type' do + it 'invokes the SendProofingNotificationJob for the enrollment' do + stub_request_passed_proofing_unsupported_id_results + + expect(pending_enrollment.notification_phone_configuration).to_not be_nil + expect(pending_enrollment.notification_sent_at).to be_nil + + expect { job.perform(Time.zone.now) }. + to have_enqueued_job(InPerson::SendProofingNotificationJob). + with(pending_enrollment.id) + end + end + end end end diff --git a/spec/jobs/in_person/send_proofing_notification_job_spec.rb b/spec/jobs/in_person/send_proofing_notification_job_spec.rb index edecbc0de8d..0313100d903 100644 --- a/spec/jobs/in_person/send_proofing_notification_job_spec.rb +++ b/spec/jobs/in_person/send_proofing_notification_job_spec.rb @@ -54,12 +54,11 @@ let(:in_person_proofing_enabled) { false } let(:in_person_send_proofing_notifications_enabled) { true } it 'returns without doing anything' do - allow(InPersonEnrollment).to receive(:find).and_return(passed_enrollment) expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_completed, ) expect(job).not_to receive(:poll) expect(job).not_to receive(:process_batch) @@ -70,13 +69,11 @@ let(:in_person_proofing_enabled) { true } let(:in_person_send_proofing_notifications_enabled) { false } it 'returns without doing anything' do - allow(InPersonEnrollment).to receive(:find).and_return(passed_enrollment) - expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_completed, ) expect(job).not_to receive(:poll) expect(job).not_to receive(:process_batch) @@ -86,16 +83,25 @@ context 'ipp and job enabled' do let(:in_person_proofing_enabled) { true } let(:in_person_send_proofing_notifications_enabled) { true } + context 'enrollment does not exist' do + it 'returns without doing anything' do + bad_id = (InPersonEnrollment.all.pluck(:id).max || 0) + 1 + expect(analytics).not_to receive( + :idv_in_person_send_proofing_notification_job_started, + ) + expect(analytics).to receive( + :idv_in_person_send_proofing_notification_job_skipped, + ) + job.perform(bad_id) + end + end context 'without notification phone notification' do it 'returns without doing anything' do - allow(InPersonEnrollment).to receive(:find). - and_return(passed_enrollment_without_notification) - expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_skipped, ) job.perform(passed_enrollment_without_notification.id) end @@ -103,85 +109,97 @@ context 'with notification phone configuration' do it 'sends notification successfully when enrollment is successful and enrollment updated' do allow(Telephony).to receive(:send_notification).and_return(sms_success_response) - allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment) freeze_time do now = Time.zone.now expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_completed, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_sent_attempted, + :idv_in_person_send_proofing_notification_attempted, ) - expect(passed_enrollment.notification_sent_at).to eq(nil) + expect(passed_enrollment.reload.notification_sent_at).to be_nil job.perform(passed_enrollment.id) - expect(passed_enrollment.notification_sent_at).to eq(now) - expect(passed_enrollment.reload_notification_phone_configuration).to eq(nil) + expect(passed_enrollment.reload.notification_sent_at).to eq(now) + expect(passed_enrollment.reload.notification_phone_configuration).to be_nil end end it 'sends notification successfully when enrollment failed' do allow(Telephony).to receive(:send_notification).and_return(sms_success_response) - allow(InPersonEnrollment).to receive(:find_by).and_return(failing_enrollment) freeze_time do now = Time.zone.now expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_completed, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_sent_attempted, + :idv_in_person_send_proofing_notification_attempted, ) job.perform(failing_enrollment.id) - expect(failing_enrollment.notification_sent_at).to eq(now) - expect(failing_enrollment.reload_notification_phone_configuration).to eq(nil) + expect(failing_enrollment.reload.notification_sent_at).to eq(now) + expect(failing_enrollment.reload.notification_phone_configuration).to be_nil end end it 'sends no notification and phone removed when enrollment expired' do allow(Telephony).to receive(:send_notification).and_return(sms_success_response) - allow(InPersonEnrollment).to receive(:find_by).and_return(expired_enrollment) freeze_time do expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_started, + :idv_in_person_send_proofing_notification_job_started, ) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_job_completed, + :idv_in_person_send_proofing_notification_job_completed, ) expect(analytics).not_to receive( - :idv_in_person_usps_proofing_results_notification_sent_attempted, + :idv_in_person_send_proofing_notification_attempted, ) job.perform(expired_enrollment.id) - expect(expired_enrollment.notification_sent_at).to be_nil - expect(expired_enrollment.reload_notification_phone_configuration).to eq(nil) + expect(expired_enrollment.reload.notification_sent_at).to be_nil + expect(expired_enrollment.reload.notification_phone_configuration).to be_nil end end end context 'when failed to send notification' do it 'logs sms send failure when number is opt out and enrollment not updated' do allow(Telephony).to receive(:send_notification).and_return(sms_opt_out_response) - allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_sent_attempted, + :idv_in_person_send_proofing_notification_attempted, ) job.perform(passed_enrollment.id) - expect(passed_enrollment.notification_sent_at).to eq(nil) + expect(passed_enrollment.reload.notification_sent_at).to be_nil end it 'logs sms send failure for delivery failure' do allow(Telephony).to receive(:send_notification).and_return(sms_failure_response) - allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment) expect(analytics).to receive( - :idv_in_person_usps_proofing_results_notification_sent_attempted, + :idv_in_person_send_proofing_notification_attempted, ) job.perform(passed_enrollment.id) - expect(passed_enrollment.notification_sent_at).to eq(nil) + expect(passed_enrollment.reload.notification_sent_at).to be_nil + end + end + context 'when an exception is raised' do + it 'logs the exception details' do + allow(InPersonEnrollment). + to receive(:find_by). + and_raise(ActiveRecord::DatabaseConnectionError) + + job.perform(passed_enrollment.id) + + expect(analytics).to have_logged_event( + 'SendProofingNotificationJob: Exception raised', + enrollment_code: nil, + enrollment_id: passed_enrollment.id, + exception_class: 'ActiveRecord::DatabaseConnectionError', + exception_message: 'Database connection error', + ) end end end diff --git a/spec/lib/telephony/pinpoint/sms_sender_spec.rb b/spec/lib/telephony/pinpoint/sms_sender_spec.rb index aade8ef3256..5353255f28d 100644 --- a/spec/lib/telephony/pinpoint/sms_sender_spec.rb +++ b/spec/lib/telephony/pinpoint/sms_sender_spec.rb @@ -108,7 +108,7 @@ def ==(other) response = subject.deliver(message: 'hello!', to: '+11234567890', country_code: 'US') expect(response.success?).to eq(false) - expect(response.error).to eq(Telephony::ThrottledError.new(raised_error_message)) + expect(response.error).to eq(Telephony::RateLimitedError.new(raised_error_message)) expect(response.extra[:delivery_status]).to eq('THROTTLED') expect(response.extra[:request_id]).to eq('fake-message-request-id') end diff --git a/spec/lib/telephony/pinpoint/voice_sender_spec.rb b/spec/lib/telephony/pinpoint/voice_sender_spec.rb index 9a73f23535f..bf73199dfdb 100644 --- a/spec/lib/telephony/pinpoint/voice_sender_spec.rb +++ b/spec/lib/telephony/pinpoint/voice_sender_spec.rb @@ -118,7 +118,7 @@ def mock_build_backup_client 'Aws::PinpointSMSVoice::Errors::LimitExceededException: This is a test message' expect(response.success?).to eq(false) - expect(response.error).to eq(Telephony::ThrottledError.new(error_message)) + expect(response.error).to eq(Telephony::RateLimitedError.new(error_message)) end end diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index 1f8876c9362..652e43f5531 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -389,10 +389,13 @@ end end - describe 'skip_notification_sent_at_set?' do + describe 'eligible_for_notification?' do let(:passed_enrollment) do create(:in_person_enrollment, :passed, :with_notification_phone_configuration) end + let(:failed_enrollment) do + create(:in_person_enrollment, :failed, :with_notification_phone_configuration) + end let(:expired_enrollment) do create(:in_person_enrollment, :expired, :with_notification_phone_configuration) end @@ -406,14 +409,16 @@ create(:in_person_enrollment, :failed) end - it 'returns false when status of passed/failed/expired and notification configuration' do - expect(passed_enrollment.skip_notification_sent_at_set?).to eq(false) - expect(expired_enrollment.skip_notification_sent_at_set?).to eq(false) + it 'returns true when status of passed/failed/expired and notification configuration' do + expect(passed_enrollment.eligible_for_notification?).to eq(true) + expect(failed_enrollment.eligible_for_notification?).to eq(true) + expect(expired_enrollment.eligible_for_notification?).to eq(true) end + it 'returns false when status of incomplete or without notification configuration' do - expect(incomplete_enrollment.skip_notification_sent_at_set?).to eq(true) - expect(passed_enrollment_without_notification.skip_notification_sent_at_set?).to eq(true) - expect(failed_enrollment_without_notification.skip_notification_sent_at_set?).to eq(true) + expect(incomplete_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) end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index afb877f08dc..d6839f54dbb 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -242,82 +242,6 @@ end end - # TODO: remove entire describe block - describe '#has_proofed_before' do - it 'is false when the user has only been activated once' do - expect(profile.activated_at).to be_nil - expect(profile.active).to eq(false) - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(false) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.has_proofed_before?).to eq(false) # won't change - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_nil # to change - - profile.activate - - expect(profile.activated_at).to be_present - expect(profile.active).to eq(true) - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(false) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.has_proofed_before?).to eq(false) # unchanged - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_present # changed - end - - it 'is true when the user is re-activated' do - existing_profile = create(:profile, user: user) - - # profile before - expect(profile.activated_at).to be_nil # to change - expect(profile.active).to eq(false) # to change - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(false) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.has_proofed_before?).to eq(false) # to change - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_nil # to change - - # existing_profile before - expect(existing_profile.activated_at).to be_nil # will change !!! - expect(existing_profile.active).to eq(false) # won't change - expect(existing_profile.deactivation_reason).to be_nil - expect(existing_profile.fraud_review_pending?).to eq(false) - expect(existing_profile.gpo_verification_pending_at).to be_nil - expect(existing_profile.initiating_service_provider).to be_nil - expect(existing_profile.verified_at).to be_nil # to change - - existing_profile.activate - profile.activate - - existing_profile.reload - profile.reload - - # profile after - expect(profile.activated_at).to be_present # changed - expect(profile.active).to eq(true) # changed - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(false) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.has_proofed_before?).to eq(true) # changed - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_present # fix pending - - # existing_profile after - - # Now, existing_profile should be deactivated - expect(existing_profile.activated_at).to be_present - expect(existing_profile.active).to eq(false) - - expect(existing_profile.deactivation_reason).to be_nil - expect(existing_profile.fraud_review_pending?).to eq(false) - expect(existing_profile.gpo_verification_pending_at).to be_nil - expect(existing_profile.initiating_service_provider).to be_nil - expect(existing_profile.verified_at).to be_present # fix pending - end - end - describe '#activate' do it 'activates current Profile, de-activates all other Profile for the user' do active_profile = create(:profile, :active, user: user) diff --git a/spec/models/suspended_email_spec.rb b/spec/models/suspended_email_spec.rb new file mode 100644 index 00000000000..bfb06ec4002 --- /dev/null +++ b/spec/models/suspended_email_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe SuspendedEmail, type: :model do + describe 'associations' do + it { should belong_to(:email_address).class_name('EmailAddress') } + end + + describe 'validations' do + it { should validate_presence_of(:digested_base_email) } + end + + describe '.generate_email_digest' do + it 'generates the correct digest for a given email' do + email = 'test@example.com' + expected_digest = Digest::SHA256.hexdigest('test@example.com') + + expect(SuspendedEmail.generate_email_digest(email)).to eq(expected_digest) + end + end + + describe '.blocked_email_address' do + context 'when the email is not blocked' do + it 'returns nil' do + email = 'not_blocked@example.com' + + expect(SuspendedEmail.find_with_email(email)).to be_nil + end + end + + context 'when the email is blocked' do + it 'returns the original email address' do + blocked_email = FactoryBot.create(:email_address, email: 'blocked@example.com') + digested_base_email = SuspendedEmail.generate_email_digest('blocked@example.com') + FactoryBot.create( + :suspended_email, + digested_base_email: digested_base_email, + email_address: blocked_email, + ) + + expect(SuspendedEmail.find_with_email('blocked@example.com')).to eq(blocked_email) + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4f488a917b6..0ce1af29431 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -689,7 +689,7 @@ end describe 'user suspension' do - let(:user) { User.new } + let(:user) { create(:user) } let(:cannot_reinstate_message) { :user_is_not_suspended } let(:cannot_suspend_message) { :user_already_suspended } @@ -768,6 +768,10 @@ UpdateUser.new(user: user, attributes: { unique_session_id: mock_session_id }).call end + it 'creates SuspendedEmail records for each email address' do + expect { user.suspend! }.to(change { SuspendedEmail.count }.by(1)) + end + it 'updates the suspended_at attribute with the current time' do expect do user.suspend! @@ -822,9 +826,17 @@ describe '#reinstate!' do before do - user.suspended_at = Time.zone.now + user.suspend! user.reinstated_at = nil end + + it 'destroys SuspendedEmail records for each email address' do + email_address = user.email_addresses.last + expect { user.reinstate! }. + to(change { SuspendedEmail.find_with_email(email_address.email) }. + from(email_address).to(nil)) + end + it 'updates the reinstated_at attribute with the current time' do expect do user.reinstate! diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb index beb44410db3..93aee571862 100644 --- a/spec/presenters/image_upload_response_presenter_spec.rb +++ b/spec/presenters/image_upload_response_presenter_spec.rb @@ -64,7 +64,7 @@ FormResponse.new( success: false, errors: { - limit: t('errors.doc_auth.throttled_heading'), + limit: t('errors.doc_auth.rate_limited_heading'), }, ) end @@ -113,7 +113,7 @@ FormResponse.new( success: false, errors: { - limit: t('errors.doc_auth.throttled_heading'), + limit: t('errors.doc_auth.rate_limited_heading'), }, extra: extra_attributes, ) @@ -123,7 +123,7 @@ expected = { success: false, result_failed: false, - errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }], + errors: [{ field: :limit, message: t('errors.doc_auth.rate_limited_heading') }], redirect: idv_session_errors_throttled_url, remaining_attempts: 0, ocr_pii: nil, @@ -141,7 +141,7 @@ expected = { success: false, result_failed: false, - errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }], + errors: [{ field: :limit, message: t('errors.doc_auth.rate_limited_heading') }], redirect: idv_hybrid_mobile_capture_complete_url, remaining_attempts: 0, ocr_pii: nil, diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index fc5a1cb1082..e0ca88a89f9 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -74,6 +74,29 @@ end end + describe '#add_failed_phone_step_number' do + it 'adds uniq phone numbers in e164 format' do + subject.add_failed_phone_step_number('+1703-555-1212') + subject.add_failed_phone_step_number('703555-7575') + + expect(subject.failed_phone_step_numbers.length).to eq(2) + + # add duplicates + subject.add_failed_phone_step_number('(703) 555-1234') + subject.add_failed_phone_step_number('1703555-1212') + + expect(subject.failed_phone_step_numbers).to eq( + ['+17035551212', '+17035557575', '+17035551234'], + ) + end + end + + describe '#failed_phone_step_numbers' do + it 'defaults to an empy array' do + expect(subject.failed_phone_step_numbers).to eq([]) + end + end + describe '#create_profile_from_applicant_with_password' do before do subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN diff --git a/spec/services/maintenance_window_spec.rb b/spec/services/maintenance_window_spec.rb deleted file mode 100644 index 9f1767b27a1..00000000000 --- a/spec/services/maintenance_window_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'rails_helper' - -RSpec.describe MaintenanceWindow do - subject(:maintenance_window) do - MaintenanceWindow.new( - start: start, - finish: finish, - now: now, - display_time_zone: display_time_zone, - ) - end - - let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') } - let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') } - let(:now) { nil } - let(:display_time_zone) { 'America/Los_Angeles' } - - describe '#active?' do - context 'when now is during the maintenance window' do - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - it { expect(maintenance_window.active?).to eq(true) } - end - - context 'when now is outside the maintenance window' do - let(:now) { '2020-12-31T00:00:00Z' } - it { expect(maintenance_window.active?).to eq(false) } - end - - context 'when both start and finish are empty' do - let(:start) { nil } - let(:finish) { nil } - - it 'is falsey' do - expect(maintenance_window.active?).to be_falsey - end - end - end - - describe '#start' do - it 'is formatted in the display_time_zone' do - expect(maintenance_window.start.time_zone.name).to eq(display_time_zone) - end - - context 'with an empty value' do - let(:start) { nil } - it { expect(maintenance_window.start).to eq(nil) } - end - end - - describe '#finish' do - it 'is formatted in the display_time_zone' do - expect(maintenance_window.finish.time_zone.name).to eq(display_time_zone) - end - - context 'with an empty value' do - let(:finish) { nil } - it { expect(maintenance_window.finish).to eq(nil) } - end - end -end diff --git a/spec/views/devise/sessions/new.html.erb_spec.rb b/spec/views/devise/sessions/new.html.erb_spec.rb index d79724d77dd..4ddfa9c7a79 100644 --- a/spec/views/devise/sessions/new.html.erb_spec.rb +++ b/spec/views/devise/sessions/new.html.erb_spec.rb @@ -151,26 +151,4 @@ ) end end - - context 'during the acuant maintenance window' do - let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') } - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') } - - before do - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start) - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish) - end - - around do |ex| - travel_to(now) { ex.run } - end - - it 'renders the warning banner and the normal form' do - render - - expect(rendered).to have_content('We are currently under maintenance') - expect(rendered).to have_selector('input.email') - end - end end diff --git a/spec/views/idv/getting_started/show.html.erb_spec.rb b/spec/views/idv/getting_started/show.html.erb_spec.rb index 1f6c1ef41f6..a63321effe8 100644 --- a/spec/views/idv/getting_started/show.html.erb_spec.rb +++ b/spec/views/idv/getting_started/show.html.erb_spec.rb @@ -35,28 +35,6 @@ end end - context 'during the acuant maintenance window' do - let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') } - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') } - - before do - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start) - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish) - end - - around do |ex| - travel_to(now) { ex.run } - end - - it 'renders the warning banner but no other content' do - render - - expect(rendered).to have_content('We are currently under maintenance') - expect(rendered).to_not have_content(t('doc_auth.headings.welcome')) - end - end - it 'includes code to track clicks on the consent checkbox' do selector = [ 'lg-click-observer[event-name="IdV: consent checkbox toggled"]', diff --git a/spec/views/idv/session_errors/throttled.html.erb_spec.rb b/spec/views/idv/session_errors/throttled.html.erb_spec.rb index 66a0778a5bb..39c18d68eef 100644 --- a/spec/views/idv/session_errors/throttled.html.erb_spec.rb +++ b/spec/views/idv/session_errors/throttled.html.erb_spec.rb @@ -46,13 +46,13 @@ context 'with liveness feature disabled' do it 'renders expected heading' do - expect(rendered).to have_text(t('errors.doc_auth.throttled_heading')) + expect(rendered).to have_text(t('errors.doc_auth.rate_limited_heading')) end end context 'with liveness feature enabled' do it 'renders expected heading' do - expect(rendered).to have_text(t('errors.doc_auth.throttled_heading')) + expect(rendered).to have_text(t('errors.doc_auth.rate_limited_heading')) end end end diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb index 7a76176812f..4de2cfaa7e9 100644 --- a/spec/views/idv/welcome/show.html.erb_spec.rb +++ b/spec/views/idv/welcome/show.html.erb_spec.rb @@ -31,28 +31,6 @@ end end - context 'during the acuant maintenance window' do - let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') } - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') } - - before do - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start) - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish) - end - - around do |ex| - travel_to(now) { ex.run } - end - - it 'renders the warning banner but no other content' do - render - - expect(rendered).to have_content('We are currently under maintenance') - expect(rendered).to_not have_content(t('doc_auth.headings.welcome')) - end - end - context 'without service provider' do it 'renders troubleshooting options' do render diff --git a/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb b/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb deleted file mode 100644 index ba063a41540..00000000000 --- a/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'shared/_maintenance_window_alert.html.erb' do - let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') } - let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') } - - before do - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start) - allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish) - end - - subject(:render_partial) do - render( - 'shared/maintenance_window_alert', - now: now, - ) { 'contents of block' } - end - - context 'during the maintenance window' do - let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') } - - it 'renders a warning and not the contents of the block' do - render_partial - - expect(rendered).to have_content('We are currently under maintenance') - - formatted_finish = l( - finish.in_time_zone('America/New_York'), - format: t('time.formats.event_timestamp_with_zone'), - ) - expect(rendered).to have_content(formatted_finish) - - expect(rendered).to_not have_content('contents of block') - end - end - - context 'outside the maintenance window' do - let(:now) { Time.zone.parse('2020-01-03T00:00:00Z') } - - it 'renders the contents of the block but no warning' do - render_partial - - expect(rendered).to have_content('contents of block') - - expect(rendered).to_not have_content('We are currently under maintenance') - end - end -end