diff --git a/app/controllers/concerns/ial2_profile_concern.rb b/app/controllers/concerns/ial2_profile_concern.rb index 679660a8d59..4fb98d2640f 100644 --- a/app/controllers/concerns/ial2_profile_concern.rb +++ b/app/controllers/concerns/ial2_profile_concern.rb @@ -18,7 +18,7 @@ def cache_profiles(raw_password) private def cache_profile_and_handle_errors(raw_password, profile) - cacher = Pii::Cacher.new(current_user, user_session) + cacher = Pii::Cacher.new(current_user, user_session, analytics:) begin cacher.save(raw_password, profile) rescue Encryption::EncryptionError => err diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index a0c5303c646..0dfa2373e13 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -85,6 +85,10 @@ def vendor_document_capture_url end end + def in_person_passports_allowed? + IdentityConfig.store.in_person_passports_enabled && document_capture_session.passport_allowed? + end + private def extra_analytics_properties diff --git a/app/controllers/idv/address_controller.rb b/app/controllers/idv/address_controller.rb index 297d635ef5c..b102efa4f08 100644 --- a/app/controllers/idv/address_controller.rb +++ b/app/controllers/idv/address_controller.rb @@ -12,7 +12,10 @@ def new analytics.idv_address_visit @address_form = build_address_form - @presenter = AddressPresenter.new(gpo_letter_requested: idv_session.gpo_letter_requested) + @presenter = AddressPresenter.new( + gpo_letter_requested: idv_session.gpo_letter_requested, + address_update_request: address_update_request?, + ) end def update @@ -75,7 +78,10 @@ def success end def failure - @presenter = AddressPresenter.new(gpo_letter_requested: idv_session.gpo_letter_requested) + @presenter = AddressPresenter.new( + gpo_letter_requested: idv_session.gpo_letter_requested, + address_update_request: address_update_request?, + ) render :new end @@ -87,6 +93,10 @@ def track_submit_event(form_result) ) end + def address_update_request? + idv_verify_info_url == request.referer + end + def address_edited? address_from_document != @address_form.updated_user_address end diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index 1cb2a61a4c9..e0221fff287 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -29,6 +29,7 @@ def index prefilled_code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code? @gpo_verify_form = GpoVerifyForm.new( + attempts_api_tracker:, user: current_user, pii: pii, resolved_authn_context_result: resolved_authn_context_result, @@ -142,6 +143,7 @@ def send_please_call_email_if_necessary(result:) def build_gpo_verify_form GpoVerifyForm.new( + attempts_api_tracker:, user: current_user, pii: pii, resolved_authn_context_result: resolved_authn_context_result, diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 7c044d9ecd8..afd0da3e6af 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -127,6 +127,7 @@ def confirm_current_password end def init_profile + reproof = current_user.has_proofed_before? profile = idv_session.create_profile_from_applicant_with_password( password, is_enhanced_ipp: resolved_authn_context_result.enhanced_ipp?, @@ -147,6 +148,7 @@ def init_profile UserAlerts::AlertUserAboutAccountVerified.call( profile: idv_session.profile, ) + attempts_api_tracker.idv_enrollment_complete(reproof:) end end diff --git a/app/controllers/idv/forgot_password_controller.rb b/app/controllers/idv/forgot_password_controller.rb index af5fce773fa..177a4e0c452 100644 --- a/app/controllers/idv/forgot_password_controller.rb +++ b/app/controllers/idv/forgot_password_controller.rb @@ -24,9 +24,10 @@ def update def reset_password(email, request_id) sign_out RequestPasswordReset.new( - email: email, - request_id: request_id, - analytics: analytics, + email:, + request_id:, + analytics:, + attempts_api_tracker:, ).perform # The user/email is always found so... session[:email] = email diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb index f2b2214bcbc..65228f802a6 100644 --- a/app/controllers/idv/in_person/address_controller.rb +++ b/app/controllers/idv/in_person/address_controller.rb @@ -50,7 +50,9 @@ def self.step_info controller: self, next_steps: [:ipp_ssn], preconditions: ->(idv_session:, user:) { - idv_session.ipp_state_id_complete? + # Handling passport navigation with checking in_person_passports_allowed? since passport + # form is not setup yet. This should be updated during LG-15985 implmentation. + idv_session.ipp_state_id_complete? || idv_session.in_person_passports_allowed? }, undo_step: ->(idv_session:, user:) do idv_session.invalidate_in_person_address_step! diff --git a/app/controllers/idv/in_person/passport_controller.rb b/app/controllers/idv/in_person/passport_controller.rb index 0b3f045d49e..74c0fec14f4 100644 --- a/app/controllers/idv/in_person/passport_controller.rb +++ b/app/controllers/idv/in_person/passport_controller.rb @@ -13,6 +13,11 @@ def show analytics.idv_in_person_proofing_passport_visited(**analytics_arguments) end + def update + enrollment.update!(document_type: :passport_book) + redirect_to idv_in_person_address_path + end + def extra_view_variables { form:, @@ -48,6 +53,10 @@ def analytics_arguments .merge(extra_analytics_properties) end + def enrollment + current_user.establishing_in_person_enrollment + end + def initialize_pii_from_user user_session['idv/in_person'] ||= {} user_session['idv/in_person']['pii_from_user'] ||= { uuid: current_user.uuid } diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb index c51b956696e..f37a8dd42f1 100644 --- a/app/controllers/idv/in_person/state_id_controller.rb +++ b/app/controllers/idv/in_person/state_id_controller.rb @@ -52,6 +52,7 @@ def update redirect_url = idv_in_person_ssn_url end + enrollment.update!(document_type: :state_id) idv_session.doc_auth_vendor = Idp::Constants::Vendors::USPS analytics.idv_in_person_proofing_state_id_submitted( @@ -158,6 +159,10 @@ def flow_params ) end + def enrollment + current_user.establishing_in_person_enrollment + end + def form @form ||= Idv::StateIdForm.new(current_user) end diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index bfe6dfce95e..c11d6217361 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -61,6 +61,7 @@ def update issuer: current_sp&.issuer, doc_auth_result: document_capture_session&.last_doc_auth_result, sponsor_id: enrollment_sponsor_id, + document_type: nil, ) render json: { success: true }, status: :ok diff --git a/app/controllers/idv/in_person_controller.rb b/app/controllers/idv/in_person_controller.rb index 72fc95749e5..5a21ae23c1a 100644 --- a/app/controllers/idv/in_person_controller.rb +++ b/app/controllers/idv/in_person_controller.rb @@ -3,6 +3,7 @@ module Idv class InPersonController < ApplicationController include Idv::AvailabilityConcern + include Idv::ChooseIdTypeConcern include RenderConditionConcern include IdvStepConcern @@ -14,7 +15,7 @@ class InPersonController < ApplicationController before_action :set_usps_form_presenter def index - if idv_session.in_person_passports_allowed? + if in_person_passports_allowed? redirect_to idv_in_person_choose_id_type_url else redirect_to idv_in_person_state_id_url @@ -22,7 +23,7 @@ def index end def update - if idv_session.in_person_passports_allowed? + if in_person_passports_allowed? redirect_to idv_in_person_choose_id_type_url else redirect_to idv_in_person_state_id_url diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb index fcdb35562bb..ea052c60208 100644 --- a/app/controllers/users/reset_passwords_controller.rb +++ b/app/controllers/users/reset_passwords_controller.rb @@ -93,9 +93,10 @@ def request_id def handle_valid_email RequestPasswordReset.new( - email: email, - request_id: request_id, - analytics: analytics, + email:, + request_id:, + analytics:, + attempts_api_tracker:, ).perform session[:email] = email diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 456aa195f45..6e5a6ff4176 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -115,9 +115,13 @@ def recaptcha_assessment_id end def recaptcha_form + return @recaptcha_form if defined?(@recaptcha_form) + existing_device = User.find_with_confirmed_email(auth_params[:email])&.devices&.exists?( + cookie_uuid: cookies[:device], + ) + @recaptcha_form ||= SignInRecaptchaForm.new( - email: auth_params[:email], - device_cookie: cookies[:device], + existing_device: existing_device, ab_test_bucket: ab_test_bucket(:RECAPTCHA_SIGN_IN, user: user_from_params), **recaptcha_form_args, ) diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index a1908c9259f..8ef9576be41 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -9,9 +9,10 @@ class GpoVerifyForm validate :validate_pending_profile attr_accessor :otp, :pii, :pii_attributes - attr_reader :user, :resolved_authn_context_result + attr_reader :attempts_api_tracker, :user, :resolved_authn_context_result - def initialize(user:, pii:, resolved_authn_context_result:, otp: nil) + def initialize(attempts_api_tracker:, user:, pii:, resolved_authn_context_result:, otp: nil) + @attempts_api_tracker = attempts_api_tracker @user = user @pii = pii @resolved_authn_context_result = resolved_authn_context_result @@ -21,6 +22,7 @@ def initialize(user:, pii:, resolved_authn_context_result:, otp: nil) def submit result = valid? fraud_check_failed = pending_profile&.fraud_pending_reason.present? + reproof = user.has_proofed_before? if result pending_profile&.remove_gpo_deactivation_reason @@ -35,6 +37,11 @@ def submit else reset_sensitive_fields end + + if pending_profile&.active? + attempts_api_tracker.idv_enrollment_complete(reproof:) + end + FormResponse.new( success: result, errors: errors, diff --git a/app/forms/sign_in_recaptcha_form.rb b/app/forms/sign_in_recaptcha_form.rb index 9bbdb422ef8..584fc345682 100644 --- a/app/forms/sign_in_recaptcha_form.rb +++ b/app/forms/sign_in_recaptcha_form.rb @@ -5,20 +5,20 @@ class SignInRecaptchaForm RECAPTCHA_ACTION = 'sign_in' - attr_reader :form_class, :form_args, :email, :recaptcha_token, :device_cookie, :ab_test_bucket, + attr_reader :form_class, :form_args, :recaptcha_token, :ab_test_bucket, :assessment_id + attr_writer :existing_device + validate :validate_recaptcha_result def initialize( - email:, - device_cookie:, + existing_device:, ab_test_bucket:, form_class: RecaptchaForm, **form_args ) - @email = email - @device_cookie = device_cookie + @existing_device = existing_device @ab_test_bucket = ab_test_bucket @form_class = form_class @form_args = form_args @@ -34,7 +34,7 @@ def submit(recaptcha_token:) def exempt? IdentityConfig.store.sign_in_recaptcha_score_threshold.zero? || ab_test_bucket != :sign_in_recaptcha || - device.present? + @existing_device end private @@ -44,10 +44,6 @@ def validate_recaptcha_result errors.merge!(recaptcha_form) if !recaptcha_response.success? end - def device - User.find_with_confirmed_email(email)&.devices&.find_by(cookie_uuid: device_cookie) - end - def score_threshold if exempt? 0.0 diff --git a/app/javascript/packages/address-search/README.md b/app/javascript/packages/address-search/README.md index 5ee0d4c89a1..ec196524b62 100644 --- a/app/javascript/packages/address-search/README.md +++ b/app/javascript/packages/address-search/README.md @@ -21,7 +21,7 @@ Requires @18f/identity-i18n. To use this component, provide callbacks to it for desired behaviors. ```typescript jsx -import AddressSearch from '@18f/identity-address-search'; +import FullAddressSearch from '@18f/identity-address-search'; // Render UI component @@ -40,6 +40,29 @@ return( ); ``` +By adding the usesErrorComponent prop to the FullAddressSearch component you can opt in to showing a specialized error message for skipping location selection when a locations endpoint error happens instead of the alert. + +```typescript jsx +import FullAddressSearch from '@18f/identity-address-search'; + +// Render UI component + +return( + <> + + +); +``` + ## License This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/app/javascript/packages/address-search/components/full-address-search-input.tsx b/app/javascript/packages/address-search/components/full-address-search-input.tsx index 31610e5493d..e564ac89c2f 100644 --- a/app/javascript/packages/address-search/components/full-address-search-input.tsx +++ b/app/javascript/packages/address-search/components/full-address-search-input.tsx @@ -17,6 +17,7 @@ export default function FullAddressSearchInput({ registerField = () => undefined, usStatesTerritories, uspsApiError, + usesErrorComponent, }: FullAddressSearchInputProps) { const { t } = useI18n(); const spinnerButtonRef = useRef(null); @@ -175,12 +176,12 @@ export default function FullAddressSearchInput({ isBig ref={spinnerButtonRef} type="submit" - onClick={uspsApiError ? handleContinue : handleSearch} + onClick={usesErrorComponent && uspsApiError ? handleContinue : handleSearch} spinOnClick={false} actionMessage={t('in_person_proofing.body.location.po_search.is_searching_message')} longWaitDurationMs={1} > - {uspsApiError + {usesErrorComponent && uspsApiError ? t('in_person_proofing.body.location.po_search.continue_button') : t('in_person_proofing.body.location.po_search.search_button')} diff --git a/app/javascript/packages/address-search/components/full-address-search.tsx b/app/javascript/packages/address-search/components/full-address-search.tsx index 1a5526901c1..e2a09e0a5de 100644 --- a/app/javascript/packages/address-search/components/full-address-search.tsx +++ b/app/javascript/packages/address-search/components/full-address-search.tsx @@ -59,6 +59,7 @@ function FullAddressSearch({ disabled={disabled} locationsURL={locationsURL} uspsApiError={apiError} + usesErrorComponent={usesErrorComponent} /> {usesErrorComponent && apiError && } {locationResults && foundAddress && !isLoadingLocations && ( diff --git a/app/javascript/packages/address-search/package.json b/app/javascript/packages/address-search/package.json index 1812b0240a0..a3f7b95c503 100644 --- a/app/javascript/packages/address-search/package.json +++ b/app/javascript/packages/address-search/package.json @@ -1,6 +1,6 @@ { "name": "@18f/identity-address-search", - "version": "3.4.0", + "version": "4.0.1", "type": "module", "private": false, "files": [ @@ -34,4 +34,4 @@ "directory": "app/javascript/packages/address-search" }, "sideEffects": false -} \ No newline at end of file +} diff --git a/app/javascript/packages/address-search/types.d.ts b/app/javascript/packages/address-search/types.d.ts index 75c7f0ae726..f029037f699 100644 --- a/app/javascript/packages/address-search/types.d.ts +++ b/app/javascript/packages/address-search/types.d.ts @@ -94,4 +94,5 @@ interface FullAddressSearchInputProps { registerField?: RegisterFieldCallback; usStatesTerritories: string[][]; uspsApiError: Error | null; + usesErrorComponent?: boolean; } diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 396bbe52d91..8a3d920c2bb 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -490,8 +490,13 @@ def handle_successful_status_update(enrollment, response) ) unless fraud_result_pending?(enrollment) + reproof = enrollment.user&.has_proofed_before? enrollment.profile&.activate_after_passing_in_person + if enrollment.profile&.active? + attempts_api_tracker(enrollment:).idv_enrollment_complete(reproof:) + end + # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) send_verified_email(enrollment:, visited_location_name: response['proofingPostOffice']) @@ -503,6 +508,18 @@ def handle_successful_status_update(enrollment, response) end end + def attempts_api_tracker(enrollment:) + AttemptsApi::Tracker.new( + enabled_for_session: enrollment.service_provider&.attempts_api_enabled?, + session_id: nil, + request: nil, + user: enrollment.user, + sp: enrollment.service_provider, + cookie_device_uuid: nil, + sp_request_uri: nil, + ) + end + def handle_passed_with_fraud_review_pending(enrollment, response) proofed_at = parse_usps_timestamp(response['transactionEndDateTime']) enrollment_outcomes[:enrollments_in_fraud_review] += 1 diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index ea81229c386..aa169627cde 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -33,6 +33,15 @@ class InPersonEnrollment < ApplicationRecord STATUS_IN_FRAUD_REVIEW.to_sym => 6, } + DOCUMENT_TYPE_STATE_ID = 'state_id' + DOCUMENT_TYPE_PASSPORT_BOOK = 'passport_book' + + # This will always be nil in the Verify-by-Mail (GPO) flow. + enum :document_type, { + DOCUMENT_TYPE_STATE_ID.to_sym => 0, + DOCUMENT_TYPE_PASSPORT_BOOK.to_sym => 1, + } + validate :profile_belongs_to_user before_save(:on_status_updated, if: :will_save_change_to_status?) diff --git a/app/presenters/idv/address_presenter.rb b/app/presenters/idv/address_presenter.rb index 61fa61e07c8..b0b68d98ad3 100644 --- a/app/presenters/idv/address_presenter.rb +++ b/app/presenters/idv/address_presenter.rb @@ -2,33 +2,36 @@ module Idv class AddressPresenter - def initialize(gpo_letter_requested:) + attr_reader :gpo_letter_requested, :address_update_request + + def initialize(gpo_letter_requested:, address_update_request:) @gpo_letter_requested = gpo_letter_requested + @address_update_request = address_update_request && !gpo_letter_requested end - attr_reader :gpo_letter_requested - def address_heading if gpo_letter_requested I18n.t('doc_auth.headings.mailing_address') + elsif address_update_request + I18n.t('doc_auth.headings.address_update') else I18n.t('doc_auth.headings.address') end end - def address_info - if gpo_letter_requested - I18n.t('doc_auth.info.mailing_address') + def update_or_continue_button + if address_update_request + I18n.t('forms.buttons.submit.update') else - I18n.t('doc_auth.info.address') + I18n.t('forms.buttons.continue') end end - def form_button_text + def address_info if gpo_letter_requested - I18n.t('forms.buttons.continue') + I18n.t('doc_auth.info.mailing_address') else - I18n.t('forms.buttons.submit.update') + I18n.t('doc_auth.info.address') end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f636030b53d..e356a8ca405 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -738,6 +738,10 @@ def external_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extr ) end + def fingerprints_rotated + track_event(:fingerprints_rotated) + end + # The user chose to "forget all browsers" def forget_all_browsers_submitted track_event('Forget All Browsers Submitted') diff --git a/app/services/attempts_api/tracker_events.rb b/app/services/attempts_api/tracker_events.rb index 3d64ec5efa7..f51e7b70442 100644 --- a/app/services/attempts_api/tracker_events.rb +++ b/app/services/attempts_api/tracker_events.rb @@ -65,6 +65,15 @@ def forgot_password_email_confirmed(success:, failure_reason: nil) ) end + # @param [String] email The user's email address + # A user has requested a password reset. + def forgot_password_email_sent(email:) + track_event( + :forgot_password_email_sent, + email:, + ) + end + # @param [Boolean] success True if new password was successfully submitted # @param [Hash>] failure_reason # A user submits a new password have requesting a password reset @@ -76,6 +85,15 @@ def forgot_password_new_password_submitted(success:, failure_reason: nil) ) end + # @param [Boolean] reproof True indicates that the user has proofed previously + # A user has completed the identity verification process and has an active profile + def idv_enrollment_complete(reproof:) + track_event( + 'idv-enrollment-complete', + reproof:, + ) + end + # A user becomes able to visit the post office for in-person proofing def idv_ipp_ready_to_verify_visit track_event('idv-ipp-ready-to-verify-visit') diff --git a/app/services/pii/cacher.rb b/app/services/pii/cacher.rb index cf47175d2b4..1e644f2cdb2 100644 --- a/app/services/pii/cacher.rb +++ b/app/services/pii/cacher.rb @@ -2,17 +2,18 @@ module Pii class Cacher - attr_reader :user, :user_session + attr_reader :user, :user_session, :analytics - def initialize(user, user_session) + def initialize(user, user_session, analytics: nil) @user = user @user_session = user_session + @analytics = analytics end def save(user_password, profile = user.active_profile) decrypted_pii = profile.decrypt_pii(user_password) if profile save_decrypted_pii(decrypted_pii, profile.id) if decrypted_pii - rotate_fingerprints(profile, decrypted_pii) if stale_fingerprints?(profile, decrypted_pii) + rotate_fingerprints_if_stale(profile, decrypted_pii) decrypted_pii end @@ -43,12 +44,18 @@ def delete private - def rotate_fingerprints(profile, pii) - KeyRotator::HmacFingerprinter.new.rotate( - user: user, - profile: profile, - pii_attributes: pii, - ) + def rotate_fingerprints_if_stale(profile, pii) + return unless profile.present? && pii.present? + pii_copy = pii_with_normalized_ssn(pii) + + if stale_fingerprints?(profile, pii_copy) + analytics&.fingerprints_rotated + KeyRotator::HmacFingerprinter.new.rotate( + user: user, + profile: profile, + pii_attributes: pii_copy, + ) + end end def stale_fingerprints?(profile, pii) @@ -67,5 +74,11 @@ def stale_compound_pii_signature?(profile, pii) return false unless compound_pii Pii::Fingerprinter.stale?(compound_pii, profile.name_zip_birth_year_signature) end + + def pii_with_normalized_ssn(pii) + pii_copy = pii.dup + pii_copy.ssn = SsnFormatter.normalize(pii_copy.ssn) + pii_copy + end end end diff --git a/app/services/request_password_reset.rb b/app/services/request_password_reset.rb index 54cf607f63c..c5ecb92d81a 100644 --- a/app/services/request_password_reset.rb +++ b/app/services/request_password_reset.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RequestPasswordReset = RedactedStruct.new( - :email, :request_id, :analytics, + :email, :request_id, :analytics, :attempts_api_tracker, keyword_init: true, allowed_members: [:request_id] ) do @@ -26,6 +26,8 @@ def perform event = PushNotification::RecoveryActivatedEvent.new(user: user) PushNotification::HttpPush.deliver(event) + + attempts_api_tracker.forgot_password_email_sent(email:) end end diff --git a/app/views/idv/address/new.html.erb b/app/views/idv/address/new.html.erb index 2d50322e5e9..e6303f5b724 100644 --- a/app/views/idv/address/new.html.erb +++ b/app/views/idv/address/new.html.erb @@ -79,9 +79,17 @@ }, ) %> - <%= f.submit @presenter.form_button_text, class: 'display-block margin-y-5' %> + + <%= f.submit @presenter.update_or_continue_button, class: 'display-block margin-y-5' %> +<% end %> + +<% if @presenter.address_update_request %> + <%= render 'idv/shared/back', step: 'verify', fallback_path: idv_verify_info_path %> +<% elsif @presenter.gpo_letter_requested %> + <%= render 'idv/shared/back', step: 'verify', fallback_path: idv_request_letter_path %> +<% else %> + <%= render 'idv/doc_auth/cancel', step: 'verify' %> <% end %> -<%= render 'idv/shared/back', step: 'verify' %> <%= javascript_packs_tag_once('formatted-fields') %> <%= javascript_packs_tag_once('state-guidance') %> diff --git a/app/views/idv/in_person/verify_info/_address_section.html.erb b/app/views/idv/in_person/verify_info/_address_section.html.erb new file mode 100644 index 00000000000..5d8e7544227 --- /dev/null +++ b/app/views/idv/in_person/verify_info/_address_section.html.erb @@ -0,0 +1,34 @@ +
+
+
+

<%= t('headings.residential_address') %>

+
+
+
<%= t('idv.form.address1') %>:
+
<%= @pii[:address1] %>
+
+
+
<%= t('idv.form.address2') %>:
+
<%= @pii[:address2].presence %>
+
+
+
<%= t('idv.form.city') %>:
+
<%= @pii[:city] %>
+
+
+
<%= t('idv.form.state') %>:
+
<%= @pii[:state] %>
+
+
+
<%= t('idv.form.zipcode') %>:
+
<%= @pii[:zipcode] %>
+
+
+
+ <%= link_to( + t('idv.buttons.change_label'), + idv_in_person_address_url, + 'aria-label': t('idv.buttons.change_address_label'), + ) %> +
+
diff --git a/app/views/idv/in_person/verify_info/_ssn_section.html.erb b/app/views/idv/in_person/verify_info/_ssn_section.html.erb new file mode 100644 index 00000000000..6dbbbe6b96d --- /dev/null +++ b/app/views/idv/in_person/verify_info/_ssn_section.html.erb @@ -0,0 +1,26 @@ +
+
+
+

<%= t('headings.ssn') %>

+
+ <%= t('idv.form.ssn') %>: + <%= render( + 'shared/masked_text', + text: SsnFormatter.format(@ssn), + masked_text: SsnFormatter.format_masked(@ssn), + accessible_masked_text: t( + 'idv.accessible_labels.masked_ssn', + first_number: @ssn[0], + last_number: @ssn[-1], + ), + toggle_label: t('forms.ssn.show'), + ) %> +
+
+ <%= link_to( + t('idv.buttons.change_label'), + idv_in_person_ssn_url, + 'aria-label': t('idv.buttons.change_ssn_label'), + ) %> +
+
diff --git a/app/views/idv/in_person/verify_info/_state_id_section.html.erb b/app/views/idv/in_person/verify_info/_state_id_section.html.erb new file mode 100644 index 00000000000..21dcf709b4a --- /dev/null +++ b/app/views/idv/in_person/verify_info/_state_id_section.html.erb @@ -0,0 +1,56 @@ +
+
+
+

<%= t('headings.state_id') %>

+
+
+
<%= t('idv.form.first_name') %>:
+
<%= @pii[:first_name] %>
+
+
+
<%= t('idv.form.last_name') %>:
+
<%= @pii[:last_name] %>
+
+
+
<%= t('idv.form.dob') %>:
+
+ <%= I18n.l(Date.parse(@pii[:dob]), format: I18n.t('time.formats.event_date')) %> +
+
+
+
<%= t('idv.form.issuing_state') %>:
+
<%= @pii[:state_id_jurisdiction] %>
+
+
+
<%= t('idv.form.id_number') %>:
+
<%= @pii[:state_id_number] %>
+
+
+
<%= t('idv.form.address1') %>:
+
<%= @pii[:identity_doc_address1] %>
+
+
+
<%= t('idv.form.address2') %>:
+
<%= @pii[:identity_doc_address2].presence %>
+
+
+
<%= t('idv.form.city') %>:
+
<%= @pii[:identity_doc_city] %>
+
+
+
<%= t('idv.form.state') %>:
+
<%= @pii[:identity_doc_address_state] %>
+
+
+
<%= t('idv.form.zipcode') %>:
+
<%= @pii[:identity_doc_zipcode] %>
+
+
+
+ <%= link_to( + idv_in_person_state_id_url, + class: 'usa-button usa-button--unstyled padding-y-1', + 'aria-label': t('idv.buttons.change_state_id_label'), + ) { t('idv.buttons.change_label') } %> +
+
diff --git a/app/views/idv/in_person/verify_info/show.html.erb b/app/views/idv/in_person/verify_info/show.html.erb index 0f5f79aa2c0..86701c7377d 100644 --- a/app/views/idv/in_person/verify_info/show.html.erb +++ b/app/views/idv/in_person/verify_info/show.html.erb @@ -23,140 +23,29 @@ locals: <%= render PageHeadingComponent.new.with_content(t('headings.verify')) %>
-
-
-
-

<%= t('headings.state_id') %>

-
-
-
<%= t('idv.form.first_name') %>:
-
<%= @pii[:first_name] %>
-
-
-
<%= t('idv.form.last_name') %>:
-
<%= @pii[:last_name] %>
-
-
-
<%= t('idv.form.dob') %>:
-
- <%= I18n.l(Date.parse(@pii[:dob]), format: I18n.t('time.formats.event_date')) %> -
-
-
-
<%= t('idv.form.issuing_state') %>:
-
<%= @pii[:state_id_jurisdiction] %>
-
-
-
<%= t('idv.form.id_number') %>:
-
<%= @pii[:state_id_number] %>
-
-
-
<%= t('idv.form.address1') %>:
-
<%= @pii[:identity_doc_address1] %>
-
-
-
<%= t('idv.form.address2') %>:
-
<%= @pii[:identity_doc_address2].presence %>
-
-
-
<%= t('idv.form.city') %>:
-
<%= @pii[:identity_doc_city] %>
-
-
-
<%= t('idv.form.state') %>:
-
<%= @pii[:identity_doc_address_state] %>
-
-
-
<%= t('idv.form.zipcode') %>:
-
<%= @pii[:identity_doc_zipcode] %>
-
-
-
- <%= link_to( - idv_in_person_state_id_url, - class: 'usa-button usa-button--unstyled padding-y-1', - 'aria-label': t('idv.buttons.change_state_id_label'), - ) { t('idv.buttons.change_label') } %> -
-
-
-
-
-

<%= t('headings.residential_address') %>

-
-
-
<%= t('idv.form.address1') %>:
-
<%= @pii[:address1] %>
-
-
-
<%= t('idv.form.address2') %>:
-
<%= @pii[:address2].presence %>
-
-
-
<%= t('idv.form.city') %>:
-
<%= @pii[:city] %>
-
-
-
<%= t('idv.form.state') %>:
-
<%= @pii[:state] %>
-
-
-
<%= t('idv.form.zipcode') %>:
-
<%= @pii[:zipcode] %>
-
-
-
- <%= link_to( - t('idv.buttons.change_label'), - idv_in_person_address_url, - 'aria-label': t('idv.buttons.change_address_label'), - ) %> -
-
-
-
-
-

<%= t('headings.ssn') %>

-
- <%= t('idv.form.ssn') %>: - <%= render( - 'shared/masked_text', - text: SsnFormatter.format(@ssn), - masked_text: SsnFormatter.format_masked(@ssn), - accessible_masked_text: t( - 'idv.accessible_labels.masked_ssn', - first_number: @ssn[0], - last_number: @ssn[-1], - ), - toggle_label: t('forms.ssn.show'), - ) %> -
-
- <%= link_to( - t('idv.buttons.change_label'), - idv_in_person_ssn_url, - 'aria-label': t('idv.buttons.change_ssn_label'), - ) %> -
-
+ <% if !@pii[:state_id_number].nil? %> + <%= render 'state_id_section', pii: @pii %> + <% end %> + <%= render 'address_section', pii: @pii %> + <%= render 'ssn_section', ssn: @ssn %>
- <%= render SpinnerButtonComponent.new( - url: idv_in_person_verify_info_path, - big: true, - wide: true, - action_message: t('idv.messages.verifying'), - method: :put, - form: { - class: 'button_to', - data: { - form_steps_wait: '', - error_message: t('idv.failure.exceptions.internal_error'), - alert_target: '#form-steps-wait-alert', - wait_step_path: idv_in_person_verify_info_path, - poll_interval_ms: IdentityConfig.store.poll_rate_for_verify_in_seconds * 1000, - }, + <%= render SpinnerButtonComponent.new( + url: idv_in_person_verify_info_path, + big: true, + wide: true, + action_message: t('idv.messages.verifying'), + method: :put, + form: { + class: 'button_to', + data: { + form_steps_wait: '', + error_message: t('idv.failure.exceptions.internal_error'), + alert_target: '#form-steps-wait-alert', + wait_step_path: idv_in_person_verify_info_path, + poll_interval_ms: IdentityConfig.store.poll_rate_for_verify_in_seconds * 1000, }, - ).with_content(t('forms.buttons.submit.default')) %> + }, + ).with_content(t('forms.buttons.submit.default')) %>
diff --git a/config/application.yml.default b/config/application.yml.default index ddf2b9a2b37..6d561995292 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -104,6 +104,8 @@ doc_auth_client_sharpness_threshold: 50 doc_auth_error_dpi_threshold: 290 doc_auth_error_glare_threshold: 40 doc_auth_error_sharpness_threshold: 40 +doc_auth_manual_upload_disabled_a_b_testing_enabled: false +doc_auth_manual_upload_disabled_a_b_testing_percent: 0 doc_auth_max_attempts: 5 doc_auth_max_capture_attempts_before_native_camera: 3 doc_auth_max_submission_attempts_before_native_camera: 3 diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index dfba4cc0605..2a91282c308 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -127,4 +127,19 @@ def self.all ) do |service_provider:, session:, user:, user_session:, **| user&.uuid end.freeze + + DOC_AUTH_MANUAL_UPLOAD_DISABLED = AbTest.new( + experiment_name: 'Doc Auth Manual Upload Disabled', + should_log: [ + 'Idv: doc auth hybrid handoff visited', + 'IdV: doc auth document_capture visited', + ], + buckets: { + manual_upload_disabled: + IdentityConfig.store.doc_auth_manual_upload_disabled_a_b_testing_enabled ? + IdentityConfig.store.doc_auth_manual_upload_disabled_a_b_testing_percent : 0, + }, + ) do |service_provider:, session:, user:, user_session:, **| + user&.uuid + end.freeze end diff --git a/config/locales/en.yml b/config/locales/en.yml index f9f919454f8..bc3d4e1b994 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -597,7 +597,8 @@ doc_auth.headers.low_resolution: Your device’s camera may not be supported. doc_auth.headers.unaccepted_id_type: Use a driver’s license or a state ID doc_auth.headers.underage: Age requirement not met doc_auth.headers.unreadable_id: We could not read your ID -doc_auth.headings.address: Update your current residential address +doc_auth.headings.address: Enter your current residential address +doc_auth.headings.address_update: Update your current residential address doc_auth.headings.back: Back of your driver’s license or state ID doc_auth.headings.capture_complete: We verified your identity document doc_auth.headings.capture_scan_warning_html: We couldn’t read the barcode on your ID. If the information below is incorrect, please %{link_html} of your ID. @@ -1284,7 +1285,7 @@ in_person_proofing.body.location.po_search.state_label: State in_person_proofing.body.location.po_search.usps_facilities_api_error_body_html: Please continue verifying your identity. You can use our to search for a participating location after you generate a barcode. in_person_proofing.body.location.po_search.usps_facilities_api_error_header: Sorry, we ran into technical difficulties and couldn’t search for a participating Post Office. in_person_proofing.body.location.po_search.usps_facilities_api_error_help_center_text: Help Center -in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: empty location icon +in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: Image of a map pin in_person_proofing.body.location.po_search.zipcode_label: ZIP Code in_person_proofing.body.location.retail_hours_heading: Retail Hours in_person_proofing.body.location.retail_hours_sat: 'Sat:' diff --git a/config/locales/es.yml b/config/locales/es.yml index 36aa06fe65e..75582002149 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -608,7 +608,8 @@ doc_auth.headers.low_resolution: Es posible que la cámara de su dispositivo no doc_auth.headers.unaccepted_id_type: Use una licencia de conducir o una identificación estatal doc_auth.headers.underage: No se cumplió con el requisito de edad doc_auth.headers.unreadable_id: No pudimos leer su identificación -doc_auth.headings.address: Actualice su domicilio actual +doc_auth.headings.address: Ingrese su domicilio actual +doc_auth.headings.address_update: Actualice su domicilio actual doc_auth.headings.back: Reverso de su licencia de conducir o identificación estatal doc_auth.headings.capture_complete: Verificamos su documento de identidad doc_auth.headings.capture_scan_warning_html: No pudimos leer el código de barras de su identificación. Si la información siguiente es incorrecta, %{link_html} de su identificación. @@ -1295,7 +1296,7 @@ in_person_proofing.body.location.po_search.state_label: Estado in_person_proofing.body.location.po_search.usps_facilities_api_error_body_html: Siga verificando su identidad. Después de generar un código de barras, puede usar nuestro para buscar un lugar participante. in_person_proofing.body.location.po_search.usps_facilities_api_error_header: Lo sentimos, tuvimos problemas técnicos y no pudimos buscar una oficina de correos participante. in_person_proofing.body.location.po_search.usps_facilities_api_error_help_center_text: Centro de ayuda -in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: empty location icon +in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: Imagen de un marcador de mapa in_person_proofing.body.location.po_search.zipcode_label: Código postal in_person_proofing.body.location.retail_hours_heading: Horario de atención al público in_person_proofing.body.location.retail_hours_sat: 'Sábado:' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6fb1b7e3e53..02ae2b6d1dd 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -597,7 +597,8 @@ doc_auth.headers.low_resolution: Il se peut que la caméra de votre appareil ne doc_auth.headers.unaccepted_id_type: Utiliser un permis de conduire ou une carte d’identité d’un État doc_auth.headers.underage: Condition d’âge non remplie doc_auth.headers.unreadable_id: Nous n’avons pas pu lire votre pièce d’identité -doc_auth.headings.address: Mettre à jour votre adresse personnelle +doc_auth.headings.address: Saisir votre adresse personnelle actuelle +doc_auth.headings.address_update: Mettre à jour votre adresse personnelle doc_auth.headings.back: Verso de votre permis de conduire ou de votre carte d’identité d’un État doc_auth.headings.capture_complete: Nous avons vérifié votre pièce d’identité doc_auth.headings.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 pièce d’identité. @@ -1284,7 +1285,7 @@ in_person_proofing.body.location.po_search.state_label: État in_person_proofing.body.location.po_search.usps_facilities_api_error_body_html: Veuillez poursuivre la procédure de vérification d’identité. Vous pouvez recourir au pour rechercher un bureau participant après avoir généré un code-barres. in_person_proofing.body.location.po_search.usps_facilities_api_error_header: Nous n’avons pas pu rechercher de bureau de poste participant pour des raisons techniques et nous en excusons. in_person_proofing.body.location.po_search.usps_facilities_api_error_help_center_text: Centre d’aide -in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: empty location icon +in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: Image d’une épingle sur une carte in_person_proofing.body.location.po_search.zipcode_label: Code postal in_person_proofing.body.location.retail_hours_heading: Heures d’ouverture in_person_proofing.body.location.retail_hours_sat: 'Sam :' diff --git a/config/locales/zh.yml b/config/locales/zh.yml index c17c2deba14..6b7efccf092 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -608,7 +608,8 @@ doc_auth.headers.low_resolution: 你设备的相机可能不受支持 doc_auth.headers.unaccepted_id_type: 使用驾驶执照或州颁发的身份证件 doc_auth.headers.underage: 不符合年龄规定 doc_auth.headers.unreadable_id: 我们无法读取你的身份证件 -doc_auth.headings.address: 更新你当前的居住地址 +doc_auth.headings.address: 输入你当前的居住地址 +doc_auth.headings.address_update: 更新你当前的居住地址 doc_auth.headings.back: 驾照或州政府颁发身份证件的背面。 doc_auth.headings.capture_complete: 我们验证了你的身份文件 doc_auth.headings.capture_scan_warning_html: 我们读取不到你身份证件上的条形码。如果以下信息不正确,请把你身份证件的%{link_html}。 @@ -1297,7 +1298,7 @@ in_person_proofing.body.location.po_search.state_label: 州 in_person_proofing.body.location.po_search.usps_facilities_api_error_body_html: 请继续验证你的身份。生成条形码后,你可以使用我们的搜索参与本项目的邮局。 in_person_proofing.body.location.po_search.usps_facilities_api_error_header: 抱歉,我们遇到了技术困难,无法搜索参与本项目的邮局。 in_person_proofing.body.location.po_search.usps_facilities_api_error_help_center_text: 帮助中心 -in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: empty location icon +in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text: 地图图钉图像 in_person_proofing.body.location.po_search.zipcode_label: 邮编 in_person_proofing.body.location.retail_hours_heading: 营业时间 in_person_proofing.body.location.retail_hours_sat: 周六: diff --git a/config/routes.rb b/config/routes.rb index a6ced018ae3..60d8b49976d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -444,11 +444,12 @@ put '/in_person' => 'in_person#update' get '/in_person/choose_id_type' => 'in_person/choose_id_type#show' put '/in_person/choose_id_type' => 'in_person/choose_id_type#update' - get '/in_person/passport' => 'in_person/passport#show' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', as: :in_person_ready_to_verify post '/in_person/usps_locations' => 'in_person/usps_locations#index' put '/in_person/usps_locations' => 'in_person/usps_locations#update' + get '/in_person/passport' => 'in_person/passport#show' + put '/in_person/passport' => 'in_person/passport#update' get '/in_person/state_id' => 'in_person/state_id#show' put '/in_person/state_id' => 'in_person/state_id#update' get '/in_person/address' => 'in_person/address#show' diff --git a/docs/attempts-api/schemas/events/RegistrationEvents.yml b/docs/attempts-api/schemas/events/RegistrationEvents.yml index b995936883d..f71721466ee 100644 --- a/docs/attempts-api/schemas/events/RegistrationEvents.yml +++ b/docs/attempts-api/schemas/events/RegistrationEvents.yml @@ -1,24 +1,12 @@ properties: - mfa-enroll-backup-code: - $ref: './registration/MfaEnrollBackupCode.yml' mfa-enroll-phone-otp-sent: $ref: './registration/MfaEnrollPhoneOtpSent.yml' mfa-enroll-code-rate-limited: $ref: './registration/MfaEnrollCodeRateLimited.yml' mfa-enroll-phone-otp-sent-rate-limited: $ref: './registration/MfaEnrollPhoneOtpSentRateLimited.yml' - mfa-enroll-phone-otp-submitted: - # note: should this event not include "submitted" for consistency? - # Or is that confusing with the "sent" event? - $ref: './registration/MfaEnrollPhoneOtpSubmitted.yml' - mfa-enroll-piv-cac: - $ref: './registration/MfaEnrollPivCac.yml' - mfa-enroll-totp: - $ref: './registration/MfaEnrollTotp.yml' - mfa-enroll-webauthn-platform: - $ref: './registration/MfaEnrollWebauthnPlatform.yml' - mfa-enroll-webauthn-roaming: - $ref: './registration/MfaEnrollWebauthnRoaming.yml' + mfa-enrolled: + $ref: './registration/MfaEnrolled.yml' user-registration-email-confirmed: $ref: './registration/UserRegistrationEmailConfirmed.yml' user-registration-email-submission-rate-limited: diff --git a/docs/attempts-api/schemas/events/SignInEvents.yml b/docs/attempts-api/schemas/events/SignInEvents.yml index ea001017876..4c1d19bb262 100644 --- a/docs/attempts-api/schemas/events/SignInEvents.yml +++ b/docs/attempts-api/schemas/events/SignInEvents.yml @@ -19,3 +19,5 @@ properties: $ref: './sign-in/MfaLoginAuthSubmitted.yml' mfa-submission-code-rate-limited: $ref: './sign-in/MfaSubmissionCodeRateLimited.yml' + session-timeout: + $ref: './sign-in/SessionTimeout.yml' diff --git a/docs/attempts-api/schemas/events/identity-proofing/IdvAddressSubmitted.yml b/docs/attempts-api/schemas/events/identity-proofing/IdvAddressSubmitted.yml index 3b901c0b326..a03dd4c6481 100644 --- a/docs/attempts-api/schemas/events/identity-proofing/IdvAddressSubmitted.yml +++ b/docs/attempts-api/schemas/events/identity-proofing/IdvAddressSubmitted.yml @@ -4,7 +4,17 @@ allOf: - $ref: '../shared/EventProperties.yml' - type: object properties: - address: + address1: + type: string + address2: + type: string + city: + type: string + state: + type: string + country: + type: string + zip: type: string address_edited: type: boolean diff --git a/docs/attempts-api/schemas/events/identity-proofing/IdvDocumentUploadSubmitted.yml b/docs/attempts-api/schemas/events/identity-proofing/IdvDocumentUploadSubmitted.yml index d6e36ccb874..2139febd112 100644 --- a/docs/attempts-api/schemas/events/identity-proofing/IdvDocumentUploadSubmitted.yml +++ b/docs/attempts-api/schemas/events/identity-proofing/IdvDocumentUploadSubmitted.yml @@ -22,7 +22,17 @@ allOf: type: string date_of_birth: type: string - address: + address1: + type: string + address2: + type: string + city: + type: string + state: + type: string + country: + type: string + zip: type: string document_front_image_file_id: type: string @@ -30,12 +40,22 @@ allOf: document_back_image_file_id: type: string description: The ID used to retrieve this image if needed + document_selfie_image_file_id: + type: string + description: The ID used to retrieve this image if needed document_front_image_encryption_key: type: string description: Randomly generated Base64-encoded key used to encrypt the front image file. document_back_image_encryption_key: type: string description: Randomly generated Base64-encoded key used to encrypt the back image file. + document_selfie_image_encryption_key: + type: string + description: Randomly generated Base64-encoded key used to encrypt the selfie image file if it exists. + liveness_checking_required: + type: boolean + description: | + Indicates whether liveness checking is required failure_reason: type: object description: | diff --git a/docs/attempts-api/schemas/events/identity-proofing/IdvEnrollmentComplete.yml b/docs/attempts-api/schemas/events/identity-proofing/IdvEnrollmentComplete.yml index 39847b4a270..f78a105d100 100644 --- a/docs/attempts-api/schemas/events/identity-proofing/IdvEnrollmentComplete.yml +++ b/docs/attempts-api/schemas/events/identity-proofing/IdvEnrollmentComplete.yml @@ -2,4 +2,9 @@ description: | When the user is identity verified allOf: - $ref: '../shared/EventProperties.yml' - - type: object \ No newline at end of file + - type: object + properties: + reproof: + type: boolean + description: | + When true, indicates that the user has proofed previously \ No newline at end of file diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollBackupCode.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollBackupCode.yml deleted file mode 100644 index 48a0b749857..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollBackupCode.yml +++ /dev/null @@ -1,9 +0,0 @@ -description: The user has selected a set of backup codes for MFA. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - success: - type: boolean - description: | - Indicates whether the backup codes were successfully generated diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollPhoneOtpSubmitted.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollPhoneOtpSubmitted.yml deleted file mode 100644 index 06eba494d7a..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollPhoneOtpSubmitted.yml +++ /dev/null @@ -1,10 +0,0 @@ -description: | - The user, after having previously been sent an OTP code during phone enrollment, has been asked to submit that code. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - success: - type: boolean - description: | - Indicates whether the entered code matched what was sent AND was not expired. diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollPivCac.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollPivCac.yml deleted file mode 100644 index 7da2146d377..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollPivCac.yml +++ /dev/null @@ -1,58 +0,0 @@ -description: | - The user, after having previously been sent an OTP code during phone enrollment, has been asked to submit that code. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - subject_dn: - type: string - description: | - The subject's Distinguished Name (DN) - failure_reason: - type: object - description: | - An OPTIONAL object. An associative array of attributes and errors if success is false - properties: - piv_cac: - type: array - description: An OPTIONAL key that describes errors with the PIV/CAC - items: - type: string - enum: - - already_associated - user: - type: array - description: An OPTIONAL key that describes errors with the user - items: - type: string - enum: - - not_found - - piv_cac_mismatch - certificate: - type: array - description: An OPTIONAL key that describes errors with the certificate - items: - type: string - enum: - - bad - - expired - - invalid - - none - - not_auth_cert - - revoked - - self-signed cert - - unverified - token: - type: array - description: An OPTIONAL key that describes errors with the certificate - - items: - type: string - enum: - - bad - - http_failure - - invalid - - missing - success: - type: boolean - description: | - Indicates whether the entered code matched what was sent AND was not expired. diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollTotp.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollTotp.yml deleted file mode 100644 index 7764ea342d9..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollTotp.yml +++ /dev/null @@ -1,9 +0,0 @@ -description: The user has enrolled a TOTP device for MFA. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - success: - type: boolean - description: | - Indicates whether the entered code matched what was sent AND was not expired. diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnPlatform.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnPlatform.yml deleted file mode 100644 index b63360991b6..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnPlatform.yml +++ /dev/null @@ -1,10 +0,0 @@ -description: The user has enrolled face or touch unlock for MFA. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - success: - type: boolean - description: | - Indicates whether the platform device was successfully added for MFA. - diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnRoaming.yml b/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnRoaming.yml deleted file mode 100644 index 085a308e7e3..00000000000 --- a/docs/attempts-api/schemas/events/registration/MfaEnrollWebauthnRoaming.yml +++ /dev/null @@ -1,10 +0,0 @@ -description: The user has enrolled a security key for MFA. -allOf: - - $ref: '../shared/EventProperties.yml' - - type: object - properties: - success: - type: boolean - description: | - Indicates whether the security key was successfully added for MFA. - diff --git a/docs/attempts-api/schemas/events/registration/MfaEnrolled.yml b/docs/attempts-api/schemas/events/registration/MfaEnrolled.yml new file mode 100644 index 00000000000..8beeaa8e61f --- /dev/null +++ b/docs/attempts-api/schemas/events/registration/MfaEnrolled.yml @@ -0,0 +1,27 @@ +description: The user has set up multi-factor authentication +allOf: + - $ref: '../shared/EventProperties.yml' + - type: object + properties: + success: + type: boolean + description: | + Indicates whether the MFA setup was successful. + mfa_device_type: + type: string + enum: + - backup_code + - otp + - piv_cac + - totp + - webauthn + - webauthn_platform + phone_number: + type: string + description: OPTIONAL UNLESS mfa_device_type is phone. The enrolled phone number. + otp_delivery_method: + type: string + enum: + - sms + - voice + description: OPTIONAL UNLESS mfa_device_type is phone. The `otp_delivery_method` is included. \ No newline at end of file diff --git a/docs/attempts-api/schemas/events/shared/EventProperties.yml b/docs/attempts-api/schemas/events/shared/EventProperties.yml index 97091c47d46..90803500355 100644 --- a/docs/attempts-api/schemas/events/shared/EventProperties.yml +++ b/docs/attempts-api/schemas/events/shared/EventProperties.yml @@ -10,9 +10,12 @@ properties: device_fingerprint: type: string description: Hashed cookie device UUID + language: + type: string + description: The locale set by the user. occurred_at: - type: integer - format: int64 + type: number + format: float64 description: The time when the event occurred. subject: $ref: './Subject.yml' diff --git a/docs/attempts-api/schemas/events/sign-in/SessionTimeout.yml b/docs/attempts-api/schemas/events/sign-in/SessionTimeout.yml new file mode 100644 index 00000000000..e7973433206 --- /dev/null +++ b/docs/attempts-api/schemas/events/sign-in/SessionTimeout.yml @@ -0,0 +1,4 @@ +description: A logged in user has been inactive long enough to cause a session timeout +allOf: + - $ref: '../shared/EventProperties.yml' + - type: object diff --git a/lib/action_account.rb b/lib/action_account.rb index 85befe68316..231f0c4fbf7 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -275,6 +275,7 @@ def run(args:, config:) profile = nil profile_fraud_review_pending_at = nil success = false + reproof = user.has_proofed_before? log_texts = [] if !user.fraud_review_pending? @@ -287,6 +288,7 @@ def run(args:, config:) success = true if profile.active? + attempts_api_tracker(profile:).idv_enrollment_complete(reproof:) UserEventCreator.new(current_user: user) .create_out_of_band_user_event(:account_verified) UserAlerts::AlertUserAboutAccountVerified.call(profile: profile) @@ -356,6 +358,18 @@ def run(args:, config:) table:, ) end + + def attempts_api_tracker(profile:) + AttemptsApi::Tracker.new( + enabled_for_session: profile.initiating_service_provider&.attempts_api_enabled?, + session_id: nil, + request: nil, + user: profile.user, + sp: profile.initiating_service_provider, + cookie_device_uuid: nil, + sp_request_uri: nil, + ) + end end class SuspendUser diff --git a/lib/identity_config.rb b/lib/identity_config.rb index eef21146616..d9f92391885 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -124,6 +124,8 @@ def self.store config.add(:doc_auth_error_glare_threshold, type: :integer) config.add(:doc_auth_error_sharpness_threshold, type: :integer) config.add(:doc_auth_max_attempts, type: :integer) + config.add(:doc_auth_manual_upload_disabled_a_b_testing_enabled, type: :boolean) + config.add(:doc_auth_manual_upload_disabled_a_b_testing_percent, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) config.add(:doc_auth_passports_enabled, type: :boolean) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 6a11e6533d4..7213b2afa1a 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -136,6 +136,11 @@ module Vendors same_address_as_id: 'true', }.freeze + MOCK_IPP_PASSPORT_APPLICANT = { + passport_number: '123456789', + passport_expiration_date: (DateTime.now.utc + 1.year).to_s, + }.freeze + MOCK_IDV_APPLICANT_WITH_PASSPORT = MOCK_IDV_APPLICANT.select do |field, _value| %i[first_name middle_name last_name dob sex].include?(field) end.merge( diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index d83e16d9589..8b4e4385402 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -433,4 +433,28 @@ it_behaves_like 'an A/B test that uses user_uuid as a discriminator' end + + describe 'DOC_AUTH_MANUAL_UPLOAD_DISABLED' do + let(:ab_test) { :DOC_AUTH_MANUAL_UPLOAD_DISABLED } + + let(:enable_ab_test) do + -> { + allow(IdentityConfig.store) + .to receive(:doc_auth_manual_upload_disabled_a_b_testing_enabled) + .and_return(true) + allow(IdentityConfig.store) + .to receive(:doc_auth_manual_upload_disabled_a_b_testing_percent) + .and_return(50) + } + end + + let(:disable_ab_test) do + -> { + allow(IdentityConfig.store).to receive(:doc_auth_manual_upload_disabled_a_b_testing_enabled) + .and_return(false) + } + end + + it_behaves_like 'an A/B test that uses user_uuid as a discriminator' + end end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index 74bbff8817a..c1f494028d4 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -281,6 +281,13 @@ def show in_person_verification_pending: false, ) end + + it 'does not track the idv_enrollment_complete event' do + stub_attempts_tracker + expect(@attempts_api_tracker).not_to receive(:idv_enrollment_complete).with(reproof: false) + + put :create, params: { user: { password: 'wrong' } } + end end it 'redirects to personal key path', :freeze_time do @@ -399,6 +406,13 @@ def show ) end + it 'tracks the idv_enrollment_complete event' do + stub_attempts_tracker + expect(@attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) + + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + end + it 'creates an `account_verified` event once per confirmation' do put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } events_count = user.events.where(event_type: :account_verified, ip: '0.0.0.0') @@ -1063,6 +1077,7 @@ def show context 'with a non-proofed user' do it 'does not track a reproofing event during initial proofing' do + # TODO: Attempts API Move the idv_reproo event to the initation of the proofing process expect(@attempts_api_tracker).not_to receive(:idv_reproof) put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } @@ -1096,6 +1111,11 @@ def show expect(@attempts_api_tracker).to receive(:idv_reproof) put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } end + + it 'tracks a reproofing event upon reproofing' do + expect(@attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: true) + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + end end context 'when not ial2' do diff --git a/spec/controllers/idv/forgot_password_controller_spec.rb b/spec/controllers/idv/forgot_password_controller_spec.rb index bbb5efd8066..3455bb3d321 100644 --- a/spec/controllers/idv/forgot_password_controller_spec.rb +++ b/spec/controllers/idv/forgot_password_controller_spec.rb @@ -25,10 +25,12 @@ before do stub_sign_in(user) + stub_attempts_tracker stub_analytics end it 'tracks appropriate events' do + expect(@attempts_api_tracker).to receive(:forgot_password_email_sent).with(email: user.email) post :update expect(@analytics).to have_logged_event('IdV: forgot password confirmed') diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb index b2b879b1905..acc89885135 100644 --- a/spec/controllers/idv/in_person/address_controller_spec.rb +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -35,9 +35,65 @@ state: state, } } end + it 'returns a valid StepInfo object' do expect(Idv::InPerson::AddressController.step_info).to be_valid end + + context 'preconditions' do + context 'when ipp_state_id steps have been completed' do + before do + allow(subject.idv_session).to receive(:ipp_state_id_complete?).and_return(true) + end + + it 'returns true' do + expect( + described_class.step_info.preconditions.call(idv_session: subject.idv_session, user:), + ).to be(true) + end + end + + context 'when ipp_state_id steps have not been completed' do + before do + allow(subject.idv_session).to receive(:ipp_state_id_complete?).and_return(false) + end + + context 'when in_person passports are allowed' do + before do + allow(subject.idv_session).to receive(:in_person_passports_allowed?).and_return(true) + end + + it 'returns true' do + expect( + described_class.step_info.preconditions.call(idv_session: subject.idv_session, user:), + ).to be(true) + end + end + + context 'when in_person passports are not allowed' do + before do + allow(subject.idv_session).to receive(:in_person_passports_allowed?).and_return(false) + end + + it 'returns false' do + expect( + described_class.step_info.preconditions.call(idv_session: subject.idv_session, user:), + ).to be(false) + end + end + end + end + + context 'undo_step' do + before do + allow(subject.idv_session).to receive(:invalidate_in_person_address_step!) + described_class.step_info.undo_step.call(idv_session: subject.idv_session, user:) + end + + it 'invalidates the ipp_address step' do + expect(subject.idv_session).to have_received(:invalidate_in_person_address_step!) + end + end end describe 'before_actions' do diff --git a/spec/controllers/idv/in_person/passport_controller_spec.rb b/spec/controllers/idv/in_person/passport_controller_spec.rb index 99e3ea5b579..fd2241c5ddf 100644 --- a/spec/controllers/idv/in_person/passport_controller_spec.rb +++ b/spec/controllers/idv/in_person/passport_controller_spec.rb @@ -8,7 +8,7 @@ create(:document_capture_session, user:, passport_status: 'requested') end let(:idv_session) { subject.idv_session } - let(:enrollment) { InPersonEnrollment.new } + let(:enrollment) { create(:in_person_enrollment, :establishing, user: user) } before do stub_sign_in(user) @@ -71,6 +71,7 @@ it 'renders the passport form' do expect(response).to render_template 'idv/in_person/passport/show' + expect(enrollment.document_type).to eq(nil) end it 'logs the idv_in_person_proofing_passport_visited event' do @@ -97,4 +98,22 @@ end end end + + describe '#update' do + context 'when in person passports are allowed' do + before do + allow(idv_session).to receive(:in_person_passports_allowed?).and_return(true) + expect(enrollment.document_type).to eq(nil) + put :update + end + + it 'sets the enrollment document type' do + expect(enrollment.document_type).to eq(InPersonEnrollment::DOCUMENT_TYPE_PASSPORT_BOOK) + end + + it 'redirects to the address form' do + expect(response).to redirect_to(idv_in_person_address_path) + end + end + end end diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb index d7753abe083..456ec0cb8c3 100644 --- a/spec/controllers/idv/in_person/state_id_controller_spec.rb +++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb @@ -5,7 +5,7 @@ include InPersonHelper let(:user) { build(:user) } - let(:enrollment) { InPersonEnrollment.new } + let(:enrollment) { create(:in_person_enrollment, :establishing, user: user) } before do stub_sign_in(user) @@ -200,6 +200,7 @@ expect(subject.idv_session.ssn).to eq(nil) expect(subject.idv_session.doc_auth_vendor).to eq(nil) + expect(enrollment.document_type).to eq(nil) expect(subject.extra_view_variables[:updating_state_id]).to eq(false) expect(response).to render_template :show end @@ -208,6 +209,7 @@ subject.idv_session.ssn = '123-45-6789' put :update, params: invalid_params + expect(enrollment.document_type).to eq(nil) expect(subject.extra_view_variables[:updating_state_id]).to eq(true) expect(response).to render_template :show end @@ -241,6 +243,12 @@ expect(subject.idv_session.doc_auth_vendor).to eq(Idp::Constants::Vendors::USPS) end + + it 'sets the enrollment document type' do + put :update, params: params + + expect(enrollment.document_type).to eq(InPersonEnrollment::DOCUMENT_TYPE_STATE_ID) + end end context 'when same_address_as_id is...' do diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index 2a0a44b825b..da20796082a 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -4,18 +4,21 @@ let(:user) { create(:user) } let(:sp) { nil } let(:in_person_proofing_enabled) { true } + let(:address) do UspsInPersonProofing::Applicant.new( address: '1600 Pennsylvania Ave', city: 'Washington', state: 'DC', zip_code: '20500' ) end + let(:fake_address) do UspsInPersonProofing::Applicant.new( address: '742 Evergreen Terrace', city: 'Springfield', state: 'MO', zip_code: '89011' ) end + let(:selected_location) do { usps_location: { @@ -40,6 +43,7 @@ describe '#index' do let(:locale) { nil } let(:proofer) { double('Proofer') } + let(:locations) do [ UspsInPersonProofing::PostOffice.new( @@ -80,6 +84,7 @@ ), ] end + subject(:response) do post :index, params: { locale: locale, address: { street_address: '1600 Pennsylvania Ave', @@ -230,6 +235,7 @@ context 'with failed connection to Faraday' do let(:exception) { Faraday::ConnectionFailed.new } + subject(:response) do post :index, params: { address: { street_address: '742 Evergreen Terrace', @@ -358,6 +364,7 @@ describe '#update' do let(:enrollment) { InPersonEnrollment.last } let(:sp) { create(:service_provider, ial: 2) } + subject(:response) { put :update, params: selected_location } context 'when legacy request body is sent with location data' do @@ -415,6 +422,7 @@ expect(enrollment.status).to eq('establishing') expect(enrollment.profile).to be_nil expect(enrollment.sponsor_id).to eq(IdentityConfig.store.usps_ipp_sponsor_id) + expect(enrollment.document_type).to eq(nil) expect(enrollment.selected_location_details).to eq( selected_location[:usps_location].as_json, ) diff --git a/spec/controllers/idv/in_person_controller_spec.rb b/spec/controllers/idv/in_person_controller_spec.rb index 3f44d4d8a5e..23d704d4c2e 100644 --- a/spec/controllers/idv/in_person_controller_spec.rb +++ b/spec/controllers/idv/in_person_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' RSpec.describe Idv::InPersonController do + include PassportApiHelpers + let(:in_person_proofing_enabled) { false } let(:sp) { nil } let(:user) { build(:user) } @@ -59,51 +61,54 @@ end context 'when user has an establishing in-person enrollment' do + let(:document_capture_session) do + DocumentCaptureSession.create(user: user, requested_at: Time.zone.now) + end + let(:document_capture_session_uuid) { document_capture_session&.uuid } before do + idv_session.document_capture_session_uuid = document_capture_session_uuid create(:in_person_enrollment, :establishing, user: user) end - it 'initializes the in-person session' do - get :index - - expect(controller.user_session['idv/in_person']).to include( - pii_from_user: { uuid: user.uuid }, - ) - end - - it 'redirects to the first step' do - get :index - - expect(response).to redirect_to idv_in_person_state_id_path - end + context 'when passports are not allowed' do + it 'initializes the in-person session' do + get :index - it 'has non-nil presenter' do - get :index + expect(controller.user_session['idv/in_person']).to include( + pii_from_user: { uuid: user.uuid }, + ) + end - expect(assigns(:presenter)).to be_kind_of(Idv::InPerson::UspsFormPresenter) - end + it 'redirects to the first step' do + get :index - context 'when in person passports are allowed' do - before do - allow(idv_session).to receive(:in_person_passports_allowed?).and_return(true) + expect(response).to redirect_to idv_in_person_state_id_path end - it 'redirects to the choose ID type page' do + it 'has non-nil presenter' do get :index - expect(response).to redirect_to idv_in_person_choose_id_type_path + expect(assigns(:presenter)).to be_kind_of(Idv::InPerson::UspsFormPresenter) end end - context 'when passports are not allowed' do + context 'when passports are allowed' do + let(:document_capture_session) do + DocumentCaptureSession.create( + user: user, requested_at: Time.zone.now, + passport_status: 'allowed' + ) + end + let(:document_capture_session_uuid) { document_capture_session&.uuid } before do - allow(idv_session).to receive(:in_person_passports_allowed?).and_return(false) + idv_session.document_capture_session_uuid = document_capture_session_uuid + create(:in_person_enrollment, :establishing, user: user) end - it 'redirects to the state ID page' do + it 'redirects to the choose id page' do get :index - expect(response).to redirect_to idv_in_person_state_id_path + expect(response).to redirect_to idv_in_person_choose_id_type_path end end end @@ -150,8 +155,17 @@ end context 'with establishing in-person enrollment' do + let(:document_capture_session) do + DocumentCaptureSession.create( + user: user, requested_at: Time.zone.now, + passport_status: 'allowed' + ) + end + let(:document_capture_session_uuid) { document_capture_session&.uuid } before do + idv_session.document_capture_session_uuid = document_capture_session_uuid create(:in_person_enrollment, :establishing, user: user) + allow(IdentityConfig.store).to receive(:in_person_passports_enabled).and_return(false) end it 'initializes the in-person session' do @@ -176,7 +190,8 @@ context 'when in person passports are allowed' do before do - allow(idv_session).to receive(:in_person_passports_allowed?).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_passports_enabled) + .and_return(true) end it 'redirects to the choose ID type page' do @@ -188,7 +203,8 @@ context 'when passports are not allowed' do before do - allow(idv_session).to receive(:in_person_passports_allowed?).and_return(false) + allow(IdentityConfig.store).to receive(:doc_auth_passports_enabled) + .and_return(false) end it 'redirects to the state ID page' do diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index fc767c1e5f3..38d2ed0aee1 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -495,12 +495,14 @@ end describe '#create' do + before do + stub_analytics + stub_attempts_tracker + end context 'no user matches email' do let(:email) { 'nonexistent@example.com' } it 'send an email to tell the user they do not have an account yet' do - stub_analytics - expect do put :create, params: { password_reset_email_form: { email: email }, @@ -525,11 +527,9 @@ let(:email_param) { { email: email } } let!(:user) { create(:user, :fully_registered, **email_param) } - before do - stub_analytics - end - it 'sends password reset email to user and tracks event' do + expect(@attempts_api_tracker).to receive(:forgot_password_email_sent).with(email_param) + expect do put :create, params: { password_reset_email_form: email_param } end.to change { ActionMailer::Base.deliveries.count }.by(1) @@ -555,11 +555,9 @@ } end - before do - stub_analytics - end - it 'sends missing user email and tracks event' do + expect(@attempts_api_tracker).not_to receive(:forgot_password_email_sent) + expect { put :create, params: params } .to change { ActionMailer::Base.deliveries.count }.by(1) @@ -579,9 +577,10 @@ context 'user is verified' do it 'captures in analytics that the user was verified' do - stub_analytics user = create(:user, :fully_registered) create(:profile, :active, :verified, user: user) + expect(@attempts_api_tracker).to receive(:forgot_password_email_sent) + .with(email: user.email) params = { password_reset_email_form: { email: user.email } } put :create, params: params @@ -598,7 +597,7 @@ context 'email is invalid' do it 'displays an error and tracks event' do - stub_analytics + expect(@attempts_api_tracker).not_to receive(:forgot_password_email_sent) params = { password_reset_email_form: { email: 'foo' } } expect { put :create, params: params } @@ -617,6 +616,8 @@ end it 'renders new if email is nil' do + expect(@attempts_api_tracker).not_to receive(:forgot_password_email_sent) + expect do post :create, params: { password_reset_email_form: { resend: false } } end.to change { ActionMailer::Base.deliveries.count }.by(0) @@ -625,6 +626,8 @@ end it 'renders new if email is a Hash' do + expect(@attempts_api_tracker).not_to receive(:forgot_password_email_sent) + post :create, params: { password_reset_email_form: { email: { foo: 'bar' } } } expect(response).to render_template(:new) diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index cf9976ad058..705e1ae4b59 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -6,6 +6,7 @@ unique_id { InPersonEnrollment.generate_unique_id } user { association :user, :fully_registered } sponsor_id { IdentityConfig.store.usps_ipp_sponsor_id } + document_type { nil } trait :establishing do profile { nil } @@ -71,5 +72,13 @@ trait :enhanced_ipp do sponsor_id { IdentityConfig.store.usps_eipp_sponsor_id } end + + trait :state_id do + document_type { :state_id } + end + + trait :passport_book do + document_type { :passport_book } + end end end diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb index e6223d34743..7bd23a5f399 100644 --- a/spec/features/idv/cancel_spec.rb +++ b/spec/features/idv/cancel_spec.rb @@ -10,17 +10,17 @@ let(:fake_analytics) { FakeAnalytics.new(user: user) } before do - start_idv_from_sp(sp) - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_agreement_step - allow_any_instance_of(ApplicationController).to receive(:analytics) do |controller| fake_analytics.session = controller.session fake_analytics end + start_idv_from_sp(sp) + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_agreement_step end it 'shows the user a cancellation message with the option to go back to the step' do + expect(page).to have_content(t('doc_auth.headings.verify_identity')) original_path = current_path click_link t('links.cancel') @@ -39,7 +39,7 @@ expect(page).to have_button(t('idv.cancel.actions.keep_going')) click_on(t('idv.cancel.actions.keep_going')) - + expect(page).to have_content(t('doc_auth.headings.lets_go')) expect(page).to have_current_path(original_path) expect(fake_analytics).to have_logged_event( 'IdV: cancellation go back', @@ -48,6 +48,7 @@ end it 'shows the user a cancellation message with the option to restart from the beginning' do + expect(page).to have_content(t('doc_auth.headings.verify_identity')) click_link t('links.cancel') expect(page).to have_content(t('idv.cancel.headings.prompt.standard')) @@ -65,6 +66,7 @@ click_on t('idv.cancel.actions.start_over') + expect(page).to have_content(t('doc_auth.instructions.getting_started')) expect(page).to have_current_path(idv_welcome_path) expect(fake_analytics).to have_logged_event( 'IdV: start over', @@ -73,6 +75,7 @@ end it 'shows a cancellation message with option to cancel and reset idv', :js do + expect(page).to have_content(t('doc_auth.headings.verify_identity')) click_link t('links.cancel') expect(page).to have_content(t('idv.cancel.headings.prompt.standard')) @@ -98,19 +101,24 @@ # After visiting /verify, expect to redirect to the first step in the IdV flow. visit idv_path + expect(page).to have_content(t('doc_auth.instructions.getting_started')) expect(page).to have_current_path(idv_welcome_path) end context 'when user has recorded proofing components' do before do complete_agreement_step + expect(page).to have_content(t('doc_auth.headings.hybrid_handoff')) complete_hybrid_handoff_step + expect(page).to have_content(t('doc_auth.headings.document_capture')) complete_document_capture_step end it 'includes proofing components in events', :js do + expect(page).to have_content(t('doc_auth.info.ssn')) click_link t('links.cancel') + expect(page).to have_content(t('idv.cancel.headings.prompt.standard')) expect(fake_analytics).to have_logged_event( 'IdV: cancellation visited', proofing_components: { document_check: 'mock', document_type: 'state_id' }, @@ -125,6 +133,7 @@ expect(page).to have_button(t('idv.cancel.actions.keep_going')) click_on t('idv.cancel.actions.keep_going') + expect(page).to have_content(t('doc_auth.info.ssn')) expect(fake_analytics).to have_logged_event( 'IdV: cancellation go back', @@ -134,6 +143,7 @@ click_link t('links.cancel') click_on t('idv.cancel.actions.start_over') + expect(page).to have_content(t('doc_auth.instructions.getting_started')) expect(fake_analytics).to have_logged_event( 'IdV: start over', @@ -191,6 +201,7 @@ ) start_idv_from_sp(sp) + expect(page).to have_content(t('doc_auth.instructions.getting_started')) expect(page).to have_current_path(idv_welcome_path) 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 167e244147c..8014a0618e4 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -135,7 +135,7 @@ .update(in_person_proofing_enabled: true) end - scenario 'In person proofing', allow_browser_log: true do + scenario 'In person proofing with state ID', allow_browser_log: true do visit_idp_from_sp_with_ial2(sp) user = sign_up_and_2fa_ial1_user diff --git a/spec/features/idv/in_person/passport_scenario_spec.rb b/spec/features/idv/in_person/passport_scenario_spec.rb index 7699e517288..99b6e367dea 100644 --- a/spec/features/idv/in_person/passport_scenario_spec.rb +++ b/spec/features/idv/in_person/passport_scenario_spec.rb @@ -25,7 +25,7 @@ allow(IdentityConfig.store).to receive(:doc_auth_passports_enabled).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_passports_percent).and_return(100) stub_health_check_settings - stub_health_check_endpoints + stub_health_check_endpoints_success end context 'when in person passports are enabled' do @@ -33,6 +33,38 @@ allow(IdentityConfig.store).to receive(:in_person_passports_enabled).and_return(true) end + it 'allows the user to access in person passport content', allow_browser_log: true do + reload_ab_tests + visit_idp_from_sp_with_ial2(service_provider) + sign_in_live_with_2fa(user) + + expect(page).to have_current_path(idv_welcome_path) + expect(page).to have_content t('doc_auth.headings.welcome', sp_name: service_provider_name) + expect(page).to have_content t('doc_auth.instructions.bullet1b') + + complete_welcome_step + + expect(page).to have_current_path(idv_agreement_path) + complete_agreement_step + + expect(page).to have_current_path(idv_how_to_verify_path) + expect(page).to have_content t('doc_auth.info.verify_online_description_passport') + + click_on t('forms.buttons.continue_ipp') + + expect(page).to have_current_path(idv_document_capture_path(step: 'how_to_verify')) + + click_on t('forms.buttons.continue') + + complete_location_step(user) + + expect(page).to have_current_path(idv_in_person_choose_id_type_path) + expect(page).to have_content t('doc_auth.headings.choose_id_type') + expect(page).to have_content t('in_person_proofing.info.choose_id_type') + expect(page).to have_content t('doc_auth.forms.id_type_preference.drivers_license') + expect(page).to have_content t('doc_auth.forms.id_type_preference.passport') + end + context 'when the user chooses the state_id path during enrollment creation' do it 'creates a state_id enrollment' do reload_ab_tests @@ -116,6 +148,118 @@ expect(page).to have_current_path(idv_in_person_passport_path) expect(page).to have_content t('in_person_proofing.headings.passport') expect(page).to have_content t('in_person_proofing.body.passport.info') + + fill_in_passport_form + + click_on t('forms.buttons.submit.default') + + expect(page).to have_current_path(idv_in_person_address_path) + + fill_out_address_form_ok + + click_on t('forms.buttons.continue') + + expect(page).to have_current_path(idv_in_person_ssn_path) + + fill_out_ssn_form_ok + + click_on t('forms.buttons.continue') + + expect(page).to have_current_path(idv_in_person_verify_info_path) + end + end + + context 'when the first DOS health check fails on the welcome page' do + before do + stub_composite_health_check_endpoint_failure + end + + it 'does not allow the user to access passport content' do + reload_ab_tests + visit_idp_from_sp_with_ial2(service_provider) + sign_in_live_with_2fa(user) + + expect(page).to have_current_path(idv_welcome_path) + expect(page).to have_content t( + 'doc_auth.headings.welcome', + sp_name: service_provider_name, + ) + expect(page).to have_content t('doc_auth.instructions.bullet1a') + + complete_welcome_step + + expect(page).to have_current_path(idv_agreement_path) + complete_agreement_step + + expect(page).to have_current_path(idv_how_to_verify_path) + + click_on t('forms.buttons.continue_ipp') + + expect(page).to have_current_path(idv_document_capture_path(step: 'how_to_verify')) + + click_on t('forms.buttons.continue') + complete_location_step(user) + + expect(page).to have_current_path(idv_in_person_state_id_url) + + expect(page).to have_content strip_nbsp( + t('in_person_proofing.headings.state_id_milestone_2'), + ) + end + end + + context 'when the second DOS health check fails after the user selects a post office' do + before do + stub_health_check_settings + # The first health check passes + stub_health_check_endpoints_success + end + + it 'directs the user to the choose id page with a deactivated passport option & warning' do + reload_ab_tests + visit_idp_from_sp_with_ial2(service_provider) + sign_in_live_with_2fa(user) + + expect(page).to have_current_path(idv_welcome_path) + expect(page).to have_content t( + 'doc_auth.headings.welcome', + sp_name: service_provider_name, + ) + expect(page).to have_content t('doc_auth.instructions.bullet1b') + + complete_welcome_step + + expect(page).to have_current_path(idv_agreement_path) + complete_agreement_step + + expect(page).to have_current_path(idv_how_to_verify_path) + expect(page).to have_content t('doc_auth.info.verify_online_description_passport') + + click_on t('forms.buttons.continue_ipp') + + expect(page).to have_current_path(idv_document_capture_path(step: 'how_to_verify')) + + click_on t('forms.buttons.continue') + # The second health check fails + stub_composite_health_check_endpoint_failure + complete_location_step(user) + + expect(page).to have_current_path(idv_in_person_choose_id_type_path) + + expect(page).to have_content strip_nbsp( + t('doc_auth.headings.choose_id_type'), + ) + expect(page).to have_content strip_nbsp( + t('doc_auth.info.dos_passport_api_down_message'), + ) + expect(page).to have_field( + 'doc_auth_choose_id_type_preference_passport', + visible: :all, + disabled: true, + ) + expect(page).to have_content strip_nbsp( + t('doc_auth.forms.id_type_preference.passport'), + ) end end end @@ -125,7 +269,7 @@ allow(IdentityConfig.store).to receive(:in_person_passports_enabled).and_return(false) end - it 'does not allow the user to access in person passport content' do + it 'does not allow the user to access in person passport content', allow_browser_log: true do reload_ab_tests visit_idp_from_sp_with_ial2(service_provider) sign_in_live_with_2fa(user) @@ -197,4 +341,24 @@ end end end + + def fill_in_passport_form + fill_in t('in_person_proofing.form.passport.surname'), + with: InPersonHelper::GOOD_LAST_NAME + fill_in t('in_person_proofing.form.passport.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME + + fill_in_memorable_date( + 'idv_in_person_passport_form[passport_dob]', + InPersonHelper::GOOD_DOB, + ) + + fill_in t('in_person_proofing.form.passport.passport_number'), + with: InPersonHelper::GOOD_PASSPORT_NUMBER + + fill_in_memorable_date( + 'idv_in_person_passport_form[passport_expiration]', + InPersonHelper::GOOD_PASSPORT_EXPIRATION_DATE, + ) + end end diff --git a/spec/features/idv/puerto_rican_address_spec.rb b/spec/features/idv/puerto_rican_address_spec.rb index ecc50c1eb0e..6ffd2f8e2ff 100644 --- a/spec/features/idv/puerto_rican_address_spec.rb +++ b/spec/features/idv/puerto_rican_address_spec.rb @@ -16,7 +16,7 @@ expect(page).to have_content(t('doc_auth.headings.address')) expect(page).to have_current_path(idv_address_path) - click_button t('forms.buttons.submit.update') + click_button t('forms.buttons.continue') expect(page).to have_content(t('headings.verify')) expect(page).to have_current_path(idv_verify_info_path) @@ -24,7 +24,7 @@ it 'does not redirect to the user to the address step after they update their SSN' do complete_ssn_step - click_button t('forms.buttons.submit.update') + click_button t('forms.buttons.continue') expect(page).to have_content(t('headings.verify')) expect(page).to have_current_path(idv_verify_info_path) diff --git a/spec/fixtures/dos/healthcheck/composite_health_fail.json b/spec/fixtures/dos/healthcheck/composite_health_fail.json new file mode 100644 index 00000000000..cd0e8ede407 --- /dev/null +++ b/spec/fixtures/dos/healthcheck/composite_health_fail.json @@ -0,0 +1,23 @@ +{ + "name": "Passport Match Process API", + "status": "DOWN", + "environment": "dev-share", + "responseTimeMs": 144, + "comments": "DOWN", + "downstreamHealth": [ + { + "name": "ViPRR System API", + "status": "Down", + "environment": "dev-share", + "comments": "DOWN", + "downstreamHealth": null + }, + { + "name": "Passport Match System API", + "status": "DOWN", + "environment": "psdd-dev-share", + "comments": "DOWN", + "downstreamHealth": null + } + ] +} diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index 915b689fcfb..ac90ea53bee 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -3,13 +3,15 @@ RSpec.describe GpoVerifyForm do subject(:form) do GpoVerifyForm.new( - user: user, + attempts_api_tracker:, + user:, pii: applicant, resolved_authn_context_result: Vot::Parser::Result.no_sp_result, otp: entered_otp, ) end + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:user) { create(:user, :fully_registered) } let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } let(:entered_otp) { otp } @@ -151,6 +153,11 @@ expect(result.extra).to include(fraud_check_failed: true) end + it 'does not track an enrollment event' do + expect(attempts_api_tracker).not_to receive(:idv_enrollment_complete) + subject.submit + end + context 'threatmetrix is not required for verification' do before do allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:disabled) @@ -172,6 +179,20 @@ result = subject.submit expect(result.extra).to include(fraud_check_failed: true) end + + it 'tracks an enrollment event' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) + subject.submit + end + + context 'the user has proofed before' do + before { create(:profile, :deactivated, user:) } + + it 'tracks an enrollment event with reproof set to true' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: true) + subject.submit + end + end end end end @@ -216,6 +237,11 @@ expect(result.to_h[:which_letter]).to eq(1) expect(result.to_h[:letter_count]).to eq(3) end + + it 'tracks an enrollment event' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) + subject.submit + end end context 'entered second code' do @@ -227,6 +253,11 @@ expect(result.to_h[:which_letter]).to eq(2) expect(result.to_h[:letter_count]).to eq(3) end + + it 'tracks an enrollment event' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) + subject.submit + end end context 'entered third code' do @@ -238,6 +269,11 @@ expect(result.to_h[:which_letter]).to eq(3) expect(result.to_h[:letter_count]).to eq(3) end + + it 'tracks an enrollment event' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) + subject.submit + end end end end diff --git a/spec/forms/sign_in_recaptcha_form_spec.rb b/spec/forms/sign_in_recaptcha_form_spec.rb index 428c3cc7593..4915924d5c7 100644 --- a/spec/forms/sign_in_recaptcha_form_spec.rb +++ b/spec/forms/sign_in_recaptcha_form_spec.rb @@ -5,15 +5,13 @@ let(:user) { create(:user, :with_authenticated_device) } let(:score_threshold_config) { 0.2 } let(:analytics) { FakeAnalytics.new } - let(:email) { user.email } + let(:existing_device) { false } let(:ab_test_bucket) { :sign_in_recaptcha } let(:recaptcha_token) { 'token' } - let(:device_cookie) { Random.hex } let(:score) { 1.0 } subject(:form) do described_class.new( - email:, - device_cookie:, + existing_device:, ab_test_bucket:, form_class: RecaptchaMockForm, analytics:, @@ -45,8 +43,7 @@ context 'with custom recaptcha form class' do subject(:form) do described_class.new( - email:, - device_cookie:, + existing_device:, ab_test_bucket:, analytics:, form_class: RecaptchaForm, @@ -74,8 +71,6 @@ let(:ab_test_bucket) { nil } it { is_expected.to eq(true) } - - it { expect(queries_database?).to eq(false) } end context 'score threshold configured at zero' do @@ -87,11 +82,9 @@ end context 'existing device for user' do - let(:device_cookie) { user.devices.first.cookie_uuid } + let(:existing_device) { true } it { is_expected.to eq(true) } - - it { expect(queries_database?).to eq(true) } end def queries_database? @@ -108,7 +101,7 @@ def queries_database? let(:score) { 0.0 } context 'existing device for user' do - let(:device_cookie) { user.devices.first.cookie_uuid } + let(:existing_device) { true } it 'is successful' do expect(response.to_h).to eq(success: true) diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index b4d7095eaa2..c68896b356a 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -73,7 +73,6 @@ class BaseTask { key: 'datetime.dotiw.minutes.one' }, # "minute is minute" in French and English { key: 'datetime.dotiw.minutes.other' }, # "minute is minute" in French and English { key: 'datetime.dotiw.words_connector' }, # " , " is only punctuation and not translated - { key: 'in_person_proofing.body.location.po_search.usps_facilities_api_error_icon_alt_text' }, { key: 'in_person_proofing.body.passport.info' }, # Translations will be updated for In-Person Proofing Passport Epic, see LG-15972 { key: 'in_person_proofing.form.passport.dob' }, # Translations will be updated for In-Person Proofing Passport Epic, see LG-15972 { key: 'in_person_proofing.form.passport.dob_hint' }, # Translations will be updated for In-Person Proofing Passport Epic, see LG-15972 diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index d2b86f07c3b..967b3be794a 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -5,9 +5,8 @@ let(:current_time) { Time.zone.now } let(:in_person_results_delay_in_hours) { 2 } - let(:analytics) do - instance_double(Analytics) - end + let(:analytics) { FakeAnalytics.new } + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:default_job_completion_analytics) do { enrollments_checked: 0, @@ -32,6 +31,7 @@ allow(analytics).to receive(:idv_in_person_usps_proofing_results_job_started) allow(analytics).to receive(:idv_in_person_usps_proofing_results_job_completed) allow(Analytics).to receive(:new).and_return(analytics) + allow(AttemptsApi::Tracker).to receive(:new).and_return(attempts_api_tracker) allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) allow(IdentityConfig.store).to receive(:in_person_results_delay_in_hours).and_return( in_person_results_delay_in_hours, @@ -2001,6 +2001,7 @@ :idv_in_person_usps_proofing_results_job_email_initiated, ) allow(user_mailer).to receive(:in_person_verified).and_return(mail_deliverer) + allow(attempts_api_tracker).to receive(:idv_enrollment_complete) subject.perform(current_time) end @@ -2047,6 +2048,12 @@ ) end + it 'tracks the successful enrollment in the attempts api' do + expect(attempts_api_tracker).to have_received(:idv_enrollment_complete).with( + reproof: false, + ) + end + it 'sends a proofing sms notification' do expect(send_proofing_notification_job).to have_received( :perform_later, @@ -2222,6 +2229,7 @@ :idv_in_person_usps_proofing_results_job_email_initiated, ) allow(user_mailer).to receive(:in_person_verified).and_return(mail_deliverer) + allow(attempts_api_tracker).to receive(:idv_enrollment_complete) subject.perform(current_time) end @@ -2268,6 +2276,12 @@ ) end + it 'tracks the successful enrollment in the attempts api' do + expect(attempts_api_tracker).to have_received(:idv_enrollment_complete).with( + reproof: false, + ) + end + it 'sends a proofing sms notification' do expect(send_proofing_notification_job).to have_received( :perform_later, @@ -2321,6 +2335,7 @@ :idv_in_person_usps_proofing_results_job_email_initiated, ) allow(user_mailer).to receive(:in_person_verified).and_return(mail_deliverer) + allow(attempts_api_tracker).to receive(:idv_enrollment_complete) subject.perform(current_time) end @@ -2367,6 +2382,12 @@ ) end + it 'tracks the successful enrollment in the attempts api' do + expect(attempts_api_tracker).to have_received(:idv_enrollment_complete).with( + reproof: false, + ) + end + it 'sends a proofing sms notification' do expect(send_proofing_notification_job).to have_received( :perform_later, @@ -2421,6 +2442,7 @@ :idv_in_person_usps_proofing_results_job_email_initiated, ) allow(user_mailer).to receive(:in_person_verified).and_return(mail_deliverer) + allow(attempts_api_tracker).to receive(:idv_enrollment_complete) subject.perform(current_time) end @@ -2467,6 +2489,12 @@ ) end + it 'tracks the successful enrollment in the attempts api' do + expect(attempts_api_tracker).to have_received(:idv_enrollment_complete).with( + reproof: false, + ) + end + it 'sends a proofing sms notification' do expect(send_proofing_notification_job).to have_received( :perform_later, diff --git a/spec/lib/action_account_spec.rb b/spec/lib/action_account_spec.rb index b4952415603..5949dfed676 100644 --- a/spec/lib/action_account_spec.rb +++ b/spec/lib/action_account_spec.rb @@ -237,9 +237,11 @@ let(:user_without_profile) { create(:user) } let(:analytics) { FakeAnalytics.new } + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } before do allow(Analytics).to receive(:new).and_return(analytics) + allow(AttemptsApi::Tracker).to receive(:new).and_return(attempts_api_tracker) end let(:args) { [user.uuid, user_without_profile.uuid, 'uuid-does-not-exist'] } @@ -251,6 +253,7 @@ expect(UserAlerts::AlertUserAboutAccountVerified).to receive(:call).with( profile: user.pending_profile, ) + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: false) profile_fraud_review_pending_at = user.pending_profile.fraud_review_pending_at @@ -286,6 +289,15 @@ ) end + context 'when a user has proofed before' do + before { create(:profile, :deactivated, user:) } + + it 'creates idv_enrollment_completed_event with reproof set to true' do + expect(attempts_api_tracker).to receive(:idv_enrollment_complete).with(reproof: true) + result + end + end + context 'when the user has a pending review from an IPP enrollment' do let!(:user) { create(:user) } let!(:enrollment) { create(:in_person_enrollment, :in_fraud_review, user: user) } diff --git a/spec/presenters/idv/address_presenter_spec.rb b/spec/presenters/idv/address_presenter_spec.rb new file mode 100644 index 00000000000..16a3e57e9a7 --- /dev/null +++ b/spec/presenters/idv/address_presenter_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Idv::AddressPresenter do + let(:gpo_letter_requested) { nil } + let(:address_update_request) { nil } + subject(:presenter) { described_class.new(gpo_letter_requested:, address_update_request:) } + + context 'address update request is true' do + let(:address_update_request) { true } + + it 'gives us the correct page heading' do + expect(presenter.address_heading).to eq(t('doc_auth.headings.address_update')) + end + + it 'gives us the correct update button' do + expect(presenter.update_or_continue_button).to eq(t('forms.buttons.submit.update')) + end + end + + context 'address update request is false' do + let(:address_update_request) { false } + + it 'gives us the correct page heading' do + expect(presenter.address_heading).to eq(t('doc_auth.headings.address')) + end + + it 'gives us the correct continue button' do + expect(presenter.update_or_continue_button).to eq(t('forms.buttons.continue')) + end + end + + context 'gpo_letter_requested is true' do + let(:gpo_letter_requested) { true } + let(:address_update_request) { false } + + it 'gives us the correct page heading' do + expect(presenter.address_heading).to eq(t('doc_auth.headings.mailing_address')) + end + + it 'gives us the correct update button' do + expect(presenter.update_or_continue_button).to eq(t('forms.buttons.continue')) + end + end + + context 'gpo_letter_requested and address_update_request are true' do + let(:gpo_letter_requested) { true } + let(:address_update_request) { true } + + it 'gives us the correct page heading' do + expect(presenter.address_heading).to eq(t('doc_auth.headings.mailing_address')) + end + + it 'gives us the correct update button' do + expect(presenter.update_or_continue_button).to eq(t('forms.buttons.continue')) + end + end +end diff --git a/spec/services/doc_auth/dos/requests/health_check_request_spec.rb b/spec/services/doc_auth/dos/requests/health_check_request_spec.rb index 839c6d1018e..e8010e739c2 100644 --- a/spec/services/doc_auth/dos/requests/health_check_request_spec.rb +++ b/spec/services/doc_auth/dos/requests/health_check_request_spec.rb @@ -11,7 +11,7 @@ def health_check_request_for(endpoint) before do stub_health_check_settings - stub_health_check_endpoints + stub_health_check_endpoints_success end shared_examples 'a DOS healthcheck endpoint' do |endpoint, success_body| diff --git a/spec/services/doc_auth/dos/responses/health_check_response_spec.rb b/spec/services/doc_auth/dos/responses/health_check_response_spec.rb index 018da4aaf18..8f945e00a83 100644 --- a/spec/services/doc_auth/dos/responses/health_check_response_spec.rb +++ b/spec/services/doc_auth/dos/responses/health_check_response_spec.rb @@ -41,7 +41,7 @@ def make_faraday_response(status:) context 'when initialized from a successful general health check response' do before do - stub_health_check_endpoints + stub_health_check_endpoints_success end let(:faraday_response) do @@ -61,7 +61,7 @@ def make_faraday_response(status:) context 'when initialized from a successful composite health check response' do before do - stub_health_check_endpoints + stub_health_check_endpoints_success end let(:faraday_response) do diff --git a/spec/services/gpo_reminder_sender_spec.rb b/spec/services/gpo_reminder_sender_spec.rb index 146dd93fed3..ac9a73e99c2 100644 --- a/spec/services/gpo_reminder_sender_spec.rb +++ b/spec/services/gpo_reminder_sender_spec.rb @@ -44,6 +44,7 @@ .first end + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:fake_analytics) { FakeAnalytics.new } let(:wait_for_reminder) { 14.days } let(:time_due_for_reminder) { Time.zone.now - wait_for_reminder } @@ -173,7 +174,8 @@ def set_reminder_sent_at(to_time) ] GpoVerifyForm.new( - user: user, + attempts_api_tracker:, + user:, pii: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE, resolved_authn_context_result: Vot::Parser::Result.no_sp_result.with( enhanced_ipp?: is_enhanced_ipp, diff --git a/spec/services/pii/cacher_spec.rb b/spec/services/pii/cacher_spec.rb index 38fee399747..e4a3777e97a 100644 --- a/spec/services/pii/cacher_spec.rb +++ b/spec/services/pii/cacher_spec.rb @@ -4,6 +4,7 @@ let(:password) { 'salty peanuts are best' } let(:user) { create(:user, :with_phone, password: password) } let(:user_session) { {}.with_indifferent_access } + let(:ssn) { '123456789' } let(:active_pii) do Pii::Attributes.new( @@ -11,7 +12,7 @@ last_name: 'Testerson', dob: '2023-01-01', zipcode: '10000', - ssn: '123-45-6789', + ssn: ssn, ) end let(:active_profile) do @@ -26,7 +27,7 @@ last_name: 'Testerson2', dob: '2023-01-01', zipcode: '10000', - ssn: '999-99-9999', + ssn: '999999999', ) end let(:pending_profile) do @@ -56,6 +57,32 @@ expect(decrypted_pending_session_pii).to eq(pending_pii.to_json) end + context 'when the original signature was based on an unnormalized SSN' do + before do + stub_analytics + end + + let(:ssn) { '123-45-6789' } + + it 'updates the ssn_signature based on the normalized form' do + old_ssn_signature = active_profile.ssn_signature + + # Create a new user object to drop the memoized encrypted attributes + reloaded_user = User.find(user.id) + + described_class.new(reloaded_user, user_session, analytics: @analytics) + .save(password, active_profile) + + active_profile.reload + + expect(active_profile.ssn_signature).to_not eq(old_ssn_signature) + expect(active_profile.ssn_signature).to eq(Pii::Fingerprinter.fingerprint('123456789')) + expect(@analytics).to have_logged_event( + :fingerprints_rotated, + ) + end + end + it 'updates PII bundle fingerprints when keys are rotated' do old_ssn_signature = active_profile.ssn_signature old_compound_pii_fingerprint = active_profile.name_zip_birth_year_signature diff --git a/spec/services/request_password_reset_spec.rb b/spec/services/request_password_reset_spec.rb index 7bec877a0d9..cf587837ada 100644 --- a/spec/services/request_password_reset_spec.rb +++ b/spec/services/request_password_reset_spec.rb @@ -2,6 +2,7 @@ RSpec.describe RequestPasswordReset do describe '#perform' do + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:user) { create(:user) } let(:request_id) { SecureRandom.uuid } let(:email_address) { user.email_addresses.first } @@ -26,7 +27,7 @@ context 'when the user is found' do subject(:perform) do - described_class.new(email:).perform + described_class.new(email:, attempts_api_tracker:).perform end before do @@ -62,6 +63,11 @@ subject end + + it 'records the attempts api event' do + expect(attempts_api_tracker).to receive(:forgot_password_email_sent).with(email:) + subject + end end context 'when the user is found, but is suspended' do @@ -124,8 +130,10 @@ impl.call(user, email, **options) end + expect(attempts_api_tracker).to receive(:forgot_password_email_sent).with(email:) + expect do - RequestPasswordReset.new(email:).perform + RequestPasswordReset.new(email:, attempts_api_tracker:).perform end .to(change { user.reload.reset_password_token }) end @@ -162,7 +170,8 @@ end it 'always finds the user with the confirmed email address' do - form = RequestPasswordReset.new(**email_param) + form = RequestPasswordReset.new(**email_param, attempts_api_tracker:) + expect(attempts_api_tracker).to receive(:forgot_password_email_sent).with(email_param) form.perform expect(form.send(:user)).to eq(@user_confirmed) @@ -174,11 +183,17 @@ it 'rate limits the email sending and logs a rate limit event' do max_attempts = IdentityConfig.store.reset_password_email_max_attempts + expect(attempts_api_tracker).to receive(:forgot_password_email_sent) + .with(email:) + .exactly(max_attempts - 1) + .times + (max_attempts - 1).times do expect do RequestPasswordReset.new( - email: email, - analytics: analytics, + email:, + analytics:, + attempts_api_tracker:, ).perform end .to(change { user.reload.reset_password_token }) @@ -187,8 +202,9 @@ # extra time, rate limited expect do RequestPasswordReset.new( - email: email, - analytics: analytics, + email:, + analytics:, + attempts_api_tracker:, ).perform end .to_not(change { user.reload.reset_password_token }) @@ -206,11 +222,17 @@ .with(PushNotification::RecoveryActivatedEvent.new(user: user)) .exactly(max_attempts - 1).times + expect(attempts_api_tracker).to receive(:forgot_password_email_sent) + .with(email:) + .exactly(max_attempts - 1) + .times + (max_attempts - 1).times do expect do RequestPasswordReset.new( - email: email, - analytics: analytics, + email:, + analytics:, + attempts_api_tracker:, ).perform end .to(change { user.reload.reset_password_token }) @@ -219,8 +241,9 @@ # extra time, rate limited expect do RequestPasswordReset.new( - email: email, - analytics: analytics, + email:, + analytics:, + attempts_api_tracker:, ).perform end .to_not(change { user.reload.reset_password_token }) diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 69939a50690..07e8c68752f 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -133,6 +133,7 @@ def mobile_device def complete_doc_auth_steps_before_ssn_step(expect_accessible: false, with_selfie: false) complete_doc_auth_steps_before_document_capture_step(expect_accessible: expect_accessible) + expect(page).to have_content(t('doc_auth.headings.document_capture')) complete_document_capture_step(with_selfie: with_selfie) expect_page_to_have_no_accessibility_violations(page) if expect_accessible end diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 8c8d940f71b..d66bdb2f9c0 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -32,6 +32,10 @@ module InPersonHelper GOOD_IDENTITY_DOC_ZIPCODE = Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS[:identity_doc_zipcode].freeze + GOOD_PASSPORT_NUMBER = Idp::Constants::MOCK_IPP_PASSPORT_APPLICANT[:passport_number].freeze + GOOD_PASSPORT_EXPIRATION_DATE = + Idp::Constants::MOCK_IPP_PASSPORT_APPLICANT[:passport_expiration_date].freeze + def fill_out_state_id_form_ok(same_address_as_id: false, first_name: GOOD_FIRST_NAME) fill_in t('in_person_proofing.form.state_id.first_name'), with: first_name fill_in t('in_person_proofing.form.state_id.last_name'), with: GOOD_LAST_NAME @@ -101,6 +105,17 @@ def search_for_post_office expect(page).to have_css('.location-collection-item') end + # Fills in a memorable date component input + # + # @param [String] field_name Name of the form plus the component name. e.g. `test_form[test_dob]` + # @param [String] date The date to enter into the input. `Format: YYYY-MM-DD` + def fill_in_memorable_date(field_name, date) + year, month, day = date.split('-') + fill_in "#{field_name}[month]", with: month + fill_in "#{field_name}[day]", with: day + fill_in "#{field_name}[year]", with: year + end + def complete_location_step(_user = nil) search_for_post_office within first('.location-collection-item') do @@ -218,9 +233,10 @@ def build_pii_before_state_id_update(same_address_as_id: 'true') end end - def mark_in_person_enrollment_passed(user) + def mark_in_person_enrollment_passed(user, document_type = :state_id) enrollment = user.in_person_enrollments.last expect(enrollment).to_not be_nil + expect(enrollment.document_type&.to_sym).to eq(document_type) enrollment.profile.activate_after_passing_in_person enrollment.update(status: :passed) end diff --git a/spec/support/passport_api_helpers.rb b/spec/support/passport_api_helpers.rb index 13368ab1fbb..05b9d4bc7f5 100644 --- a/spec/support/passport_api_helpers.rb +++ b/spec/support/passport_api_helpers.rb @@ -8,7 +8,7 @@ def stub_health_check_settings .and_return(composite_health_check_endpoint) end - def stub_health_check_endpoints + def stub_health_check_endpoints_success stub_request(:get, general_health_check_endpoint) .to_return_json(body: successful_api_general_health_check_body) @@ -16,6 +16,11 @@ def stub_health_check_endpoints .to_return_json(body: successful_api_composite_health_check_body) end + def stub_composite_health_check_endpoint_failure + stub_request(:get, composite_health_check_endpoint) + .to_return_json(body: failed_api_composite_health_check_body) + end + def general_health_check_endpoint 'https://dos-health-check-endpoint.test' end @@ -45,6 +50,17 @@ def successful_api_composite_health_check_body ), ) end + + def failed_api_composite_health_check_body + JSON.parse( + File.read( + Rails.root.join( + 'spec', 'fixtures', 'dos', 'healthcheck', + 'composite_health_fail.json' + ), + ), + ) + end end def self.included(base) diff --git a/spec/views/idv/address/new.html.erb_spec.rb b/spec/views/idv/address/new.html.erb_spec.rb index 57ff87c585d..c7c0d088b40 100644 --- a/spec/views/idv/address/new.html.erb_spec.rb +++ b/spec/views/idv/address/new.html.erb_spec.rb @@ -1,12 +1,20 @@ require 'rails_helper' RSpec.describe 'idv/address/new' do + let(:user) { build(:user) } let(:parsed_page) { Nokogiri::HTML.parse(rendered) } let(:gpo_letter_requested) { nil } + let(:address_update_request) { nil } shared_examples 'valid address page and form' do before do - assign(:presenter, Idv::AddressPresenter.new(gpo_letter_requested: gpo_letter_requested)) + allow(view).to receive(:current_user).and_return(user) + assign( + :presenter, Idv::AddressPresenter.new( + gpo_letter_requested: gpo_letter_requested, + address_update_request: address_update_request, + ) + ) assign(:address_form, Idv::AddressForm.new({})) render end @@ -15,9 +23,18 @@ if gpo_letter_requested expect(parsed_page).to have_content(t('doc_auth.headings.mailing_address')) expect(parsed_page).to have_content(t('doc_auth.info.mailing_address')) + expect(parsed_page).to have_content(t('forms.buttons.continue')) + expect(parsed_page).to have_link(t('forms.buttons.back'), href: idv_request_letter_path) + elsif address_update_request + expect(parsed_page).to have_content(t('doc_auth.headings.address_update')) + expect(parsed_page).to have_content(t('doc_auth.info.address')) + expect(parsed_page).to have_content(t('forms.buttons.submit.update')) + expect(parsed_page).to have_link(t('forms.buttons.back'), href: idv_verify_info_path) else expect(parsed_page).to have_content(t('doc_auth.headings.address')) expect(parsed_page).to have_content(t('doc_auth.info.address')) + expect(parsed_page).to have_content(t('forms.buttons.continue')) + expect(parsed_page).to have_link(t('links.cancel')) end end @@ -99,7 +116,7 @@ end end - context 'when the user is not requesting a GPO letter' do + context 'when user is not requesting an update' do it_behaves_like 'valid address page and form' end context 'when the user is requesting a GPO letter' do @@ -107,4 +124,10 @@ it_behaves_like 'valid address page and form' end + + context 'whene user is requesting an address update' do + let(:address_update_request) { true } + + it_behaves_like 'valid address page and form' + end end diff --git a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb index a855c021384..a6f8d614fb0 100644 --- a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb +++ b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb @@ -4,6 +4,7 @@ let(:user) do create(:user) end + let(:attempts_api_tracker) { AttemptsApiTrackingHelper::FakeAttemptsTracker.new } let(:pii) do {} @@ -19,8 +20,9 @@ allow(view).to receive(:step_indicator_steps).and_return({}) @gpo_verify_form = GpoVerifyForm.new( - user: user, - pii: pii, + attempts_api_tracker:, + user:, + pii:, resolved_authn_context_result: Vot::Parser::Result.no_sp_result, otp: '1234', )