diff --git a/.gitignore b/.gitignore index 260d1f099d0..a253a521441 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ .ruby-version .vagrant .capistrano +.tool-versions # avoid checking in stray files *.bak diff --git a/Gemfile b/Gemfile index f13db8b387a..ec26b54fad6 100644 --- a/Gemfile +++ b/Gemfile @@ -73,7 +73,6 @@ gem 'stringex', require: false gem 'strong_migrations', '>= 0.4.2' gem 'subprocess', require: false gem 'terminal-table', require: false -gem 'uglifier', '~> 4.2' gem 'valid_email', '>= 0.1.3' gem 'view_component', '~> 3.0.0' gem 'webauthn', '~> 2.5.2' diff --git a/Gemfile.lock b/Gemfile.lock index 4b04e5cc7e1..8e3e2ab218a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -286,7 +286,6 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - execjs (2.8.1) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) @@ -650,8 +649,6 @@ GEM openssl-signature_algorithm (~> 1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.8) @@ -818,7 +815,6 @@ DEPENDENCIES subprocess tableparser terminal-table - uglifier (~> 4.2) valid_email (>= 0.1.3) view_component (~> 3.0.0) webauthn (~> 2.5.2) diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 594efb8c068..5ce81998618 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -11,7 +11,6 @@ // Temporary: To be backported to design system. Unstyled buttons should inherit the appearance // of a link. display: inline; - width: auto; } .usa-button:disabled.usa-button--active, diff --git a/app/assets/stylesheets/components/_list.scss b/app/assets/stylesheets/components/_list.scss index 389920d1e7b..4e69fe67efd 100644 --- a/app/assets/stylesheets/components/_list.scss +++ b/app/assets/stylesheets/components/_list.scss @@ -6,7 +6,6 @@ background-image: url('/alert/success.svg'); background-repeat: no-repeat; content: ''; - display: inline-block; float: left; height: 1rem; margin-top: 0.33rem; diff --git a/app/controllers/concerns/idv/ab_test_analytics_concern.rb b/app/controllers/concerns/idv/ab_test_analytics_concern.rb index b5b855059be..f99e767c9dd 100644 --- a/app/controllers/concerns/idv/ab_test_analytics_concern.rb +++ b/app/controllers/concerns/idv/ab_test_analytics_concern.rb @@ -8,6 +8,7 @@ def ab_test_analytics_buckets buckets = {} if defined?(idv_session) buckets[:skip_hybrid_handoff] = idv_session&.skip_hybrid_handoff + buckets[:phone_with_camera] = idv_session&.phone_with_camera end buckets.merge(acuant_sdk_ab_test_analytics_args). diff --git a/app/controllers/concerns/idv/phone_question_ab_test_concern.rb b/app/controllers/concerns/idv/phone_question_ab_test_concern.rb index 69a8e082d9a..166bad98aa0 100644 --- a/app/controllers/concerns/idv/phone_question_ab_test_concern.rb +++ b/app/controllers/concerns/idv/phone_question_ab_test_concern.rb @@ -14,10 +14,9 @@ def phone_question_user def maybe_redirect_for_phone_question_ab_test return if phone_question_ab_test_bucket != :show_phone_question - return if request.referer == idv_phone_question_url - return if request.referer == idv_link_sent_url - return if request.referer == idv_hybrid_handoff_url - return if request.referer == idv_hybrid_handoff_url(redo: true) + + return if !defined?(idv_session) + return if !idv_session.phone_with_camera.nil? redirect_to idv_phone_question_url end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index b8ba20ca6f2..d30fbda51d7 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -119,4 +119,19 @@ def extra_analytics_properties end extra end + + def flow_policy + @flow_policy ||= Idv::FlowPolicy.new(idv_session: idv_session, user: current_user) + end + + def confirm_step_allowed + return if flow_policy.controller_allowed?(controller: self.class) + + redirect_to url_for_latest_step + end + + def url_for_latest_step + step_info = flow_policy.info_for_latest_step + url_for(controller: step_info.controller, action: step_info.action) + end end diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index bcffa3f369f..0f082a8b0c1 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -27,7 +27,7 @@ def confirm_not_rate_limited_for_phone_address_verification private def confirm_not_rate_limited_for_phone_and_letter_address_verification - if idv_attempter_rate_limited?(:proof_address) && Idv::GpoMail.new(current_user).mail_spammed? + if idv_attempter_rate_limited?(:proof_address) && Idv::GpoMail.new(current_user).rate_limited? rate_limit_redirect!(:proof_address) return true end diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index b83081e0e0c..7ca0cf2a7e6 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -4,7 +4,7 @@ class AgreementController < ApplicationController include StepIndicatorConcern before_action :confirm_not_rate_limited - before_action :confirm_welcome_step_complete + before_action :confirm_step_allowed before_action :confirm_document_capture_not_complete def show @@ -38,6 +38,15 @@ def update end end + def self.step_info + Idv::StepInfo.new( + key: :agreement, + controller: controller_name, + next_steps: [:hybrid_handoff, :document_capture, :phone_question], + preconditions: ->(idv_session:, user:) { idv_session.welcome_visited }, + ) + end + private def analytics_arguments @@ -60,17 +69,5 @@ def skip_to_capture def consent_form_params params.require(:doc_auth).permit(:idv_consent_given) end - - def confirm_welcome_step_complete - return if idv_session.welcome_visited - - redirect_to idv_welcome_url - end - - def confirm_agreement_needed - return unless idv_session.idv_consent_given - - redirect_to idv_hybrid_handoff_url - end end end diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index 3bad1e7b7da..83abde6a26f 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -30,7 +30,7 @@ def index gpo_mail = Idv::GpoMail.new(current_user) @can_request_another_letter = FeatureManagement.gpo_verification_enabled? && - !gpo_mail.mail_spammed? && + !gpo_mail.rate_limited? && !gpo_mail.profile_too_old? if pii_locked? diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index 87aaf28191c..2bcfd919ff4 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -8,7 +8,7 @@ class RequestLetterController < ApplicationController before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed before_action :confirm_user_completed_idv_profile_step - before_action :confirm_mail_not_spammed + before_action :confirm_mail_not_rate_limited before_action :confirm_profile_not_too_old def index @@ -82,8 +82,8 @@ def first_letter_requested_at current_user.gpo_verification_pending_profile&.gpo_verification_pending_at end - def confirm_mail_not_spammed - redirect_to idv_enter_password_url if gpo_mail_service.mail_spammed? + def confirm_mail_not_rate_limited + redirect_to idv_enter_password_url if gpo_mail_service.rate_limited? end def confirm_user_completed_idv_profile_step diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index cc11c6c4967..ba042717feb 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -7,6 +7,7 @@ class DocumentCaptureController < ApplicationController include PhoneQuestionAbTestConcern before_action :confirm_not_rate_limited, except: [:update] + before_action :confirm_step_allowed before_action :confirm_hybrid_handoff_complete before_action :confirm_document_capture_needed before_action :override_csp_to_allow_acuant @@ -52,6 +53,15 @@ def extra_view_variables ) end + def self.step_info + Idv::StepInfo.new( + key: :document_capture, + controller: controller_name, + next_steps: [:success], # [:ssn], + preconditions: ->(idv_session:, user:) { idv_session.flow_path == 'standard' }, + ) + end + private def confirm_hybrid_handoff_complete diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 0a6f20227e3..00cd8bc5656 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -7,7 +7,7 @@ class HybridHandoffController < ApplicationController before_action :confirm_not_rate_limited before_action :confirm_verify_info_step_needed - before_action :confirm_agreement_step_complete + before_action :confirm_step_allowed before_action :confirm_hybrid_handoff_needed, only: :show before_action :maybe_redirect_for_phone_question_ab_test, only: :show @@ -35,6 +35,15 @@ def update end end + def self.step_info + Idv::StepInfo.new( + key: :hybrid_handoff, + controller: controller_name, + next_steps: [:link_sent, :document_capture], + preconditions: ->(idv_session:, user:) { idv_session.idv_consent_given }, + ) + end + def handle_phone_submission return rate_limited_failure if rate_limiter.limited? rate_limiter.increment! @@ -190,12 +199,6 @@ def failure(message, extra = nil) FormResponse.new(**form_response_params) end - def confirm_agreement_step_complete - return if idv_session.idv_consent_given - - redirect_to idv_agreement_url - end - def formatted_destination_phone raw_phone = params.require(:doc_auth).permit(:phone) PhoneFormatter.format(raw_phone, country_code: 'US') diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 37d991ce058..41c0302ff00 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -55,7 +55,10 @@ def analytics_arguments step: 'document_capture', analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, - }.merge(ab_test_analytics_buckets) + }.merge( + ab_test_analytics_buckets, + phone_with_camera, + ) end def handle_stored_result @@ -82,6 +85,10 @@ def redo_document_capture_pending? document_capture_session.requested_at > stored_result.captured_at end + + def phone_with_camera + { phone_with_camera: phone_question_ab_test_bucket == :show_phone_question ? true : nil } + end end end end diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb new file mode 100644 index 00000000000..cb667bb74d3 --- /dev/null +++ b/app/controllers/idv/in_person/address_controller.rb @@ -0,0 +1,80 @@ +module Idv + module InPerson + class AddressController < ApplicationController + include IdvStepConcern + + before_action :render_404_if_in_person_residential_address_controller_enabled_not_set + before_action :confirm_in_person_state_id_step_complete + before_action :confirm_in_person_address_step_needed + + def show + analytics.idv_in_person_proofing_address_visited(**analytics_arguments) + + render :show, locals: extra_view_variables + end + + def extra_view_variables + { + form:, + pii:, + updating_address: updating_address?, + } + end + + private + + def flow_session + user_session.fetch('idv/in_person', {}) + end + + def updating_address? + flow_session[:pii_from_user].has_key?(:address1) && user_session[:idv].has_key?(:ssn) + end + + def pii + data = flow_session[:pii_from_user] + data = data.merge(flow_params) if params.has_key?(:in_person_address) + data.deep_symbolize_keys + end + + def form + @form ||= Idv::InPerson::AddressForm.new + end + + def flow_params + params.require(:in_person_address).permit( + *Idv::InPerson::AddressForm::ATTRIBUTES, + ) + end + + def form_submit + form.submit(flow_params) + end + + def analytics_arguments + { + flow_path: flow_path, + step: 'address', + analytics_id: 'In Person Proofing', + irs_reproofing: irs_reproofing?, + } + end + + def render_404_if_in_person_residential_address_controller_enabled_not_set + render_not_found unless + IdentityConfig.store.in_person_residential_address_controller_enabled + end + + def confirm_in_person_state_id_step_complete + return if pii_from_user&.has_key?(:identity_doc_address1) + redirect_to idv_in_person_step_url(step: :state_id) + end + + def confirm_in_person_address_step_needed + return if pii_from_user && pii_from_user[:same_address_as_id] == 'false' && + !pii_from_user.has_key?(:address1) + redirect_to idv_in_person_ssn_url + end + end + end +end diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb index 49ce8c27bb4..b9c4b31c767 100644 --- a/app/controllers/idv/in_person/ssn_controller.rb +++ b/app/controllers/idv/in_person/ssn_controller.rb @@ -89,7 +89,11 @@ def updating_ssn? def confirm_in_person_address_step_complete return if pii_from_user && pii_from_user[:address1].present? - redirect_to idv_in_person_step_url(step: :address) + if IdentityConfig.store.in_person_residential_address_controller_enabled + redirect_to idv_in_person_proofing_address_url + else + redirect_to idv_in_person_step_url(step: :address) + end end end end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 3ec8f07177f..a2e10c742b0 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -6,6 +6,7 @@ class LinkSentController < ApplicationController include PhoneQuestionAbTestConcern before_action :confirm_not_rate_limited + before_action :confirm_step_allowed before_action :confirm_hybrid_handoff_complete before_action :confirm_document_capture_needed @@ -38,6 +39,15 @@ def extra_view_variables ) end + def self.step_info + Idv::StepInfo.new( + key: :link_sent, + controller: controller_name, + next_steps: [:success], # [:ssn], + preconditions: ->(idv_session:, user:) { idv_session.flow_path == 'hybrid' }, + ) + end + private def confirm_hybrid_handoff_complete diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 2a41a4399cd..672a8aa2538 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -217,7 +217,7 @@ def formatted_previous_phone_step_params_phone def gpo_letter_available return @gpo_letter_available if defined?(@gpo_letter_available) @gpo_letter_available ||= FeatureManagement.gpo_verification_enabled? && - !Idv::GpoMail.new(current_user).mail_spammed? + !Idv::GpoMail.new(current_user).rate_limited? end # Migrated from otp_delivery_method_controller diff --git a/app/controllers/idv/phone_errors_controller.rb b/app/controllers/idv/phone_errors_controller.rb index 3322a2d113a..1aa30734735 100644 --- a/app/controllers/idv/phone_errors_controller.rb +++ b/app/controllers/idv/phone_errors_controller.rb @@ -72,7 +72,7 @@ def track_event(type:) def set_gpo_letter_available return @gpo_letter_available if defined?(@gpo_letter_available) @gpo_letter_available ||= FeatureManagement.gpo_verification_enabled? && - !Idv::GpoMail.new(current_user).mail_spammed? + !Idv::GpoMail.new(current_user).rate_limited? end # rubocop:enable Naming/MemoizedInstanceVariableName end diff --git a/app/controllers/idv/phone_question_controller.rb b/app/controllers/idv/phone_question_controller.rb index 1c3e59cc82a..d8408299714 100644 --- a/app/controllers/idv/phone_question_controller.rb +++ b/app/controllers/idv/phone_question_controller.rb @@ -6,7 +6,7 @@ class PhoneQuestionController < ApplicationController before_action :confirm_not_rate_limited before_action :confirm_verify_info_step_needed - before_action :confirm_agreement_step_complete + before_action :confirm_step_allowed before_action :confirm_hybrid_handoff_needed, only: :show def show @@ -16,32 +16,34 @@ def show end def phone_with_camera - analytics.idv_doc_auth_phone_question_submitted( - **analytics_arguments. - merge(phone_with_camera: true), - ) + idv_session.phone_with_camera = true + analytics.idv_doc_auth_phone_question_submitted(**analytics_arguments) redirect_to idv_hybrid_handoff_url end def phone_without_camera idv_session.flow_path = 'standard' - analytics.idv_doc_auth_phone_question_submitted( - **analytics_arguments. - merge(phone_with_camera: false), - ) + idv_session.phone_with_camera = false + analytics.idv_doc_auth_phone_question_submitted(**analytics_arguments) redirect_to idv_document_capture_url end - private - - def confirm_agreement_step_complete - return if idv_session.idv_consent_given - - redirect_to idv_agreement_url + def self.step_info + Idv::StepInfo.new( + key: :phone_question, + controller: controller_name, + next_steps: [:hybrid_handoff, :document_capture], + preconditions: ->(idv_session:, user:) do + AbTests::IDV_PHONE_QUESTION.bucket(user.uuid) == :show_phone_question && + idv_session.idv_consent_given + end, + ) end + private + def analytics_arguments { step: 'phone_question', diff --git a/app/controllers/idv/welcome_controller.rb b/app/controllers/idv/welcome_controller.rb index 5eaa1537da2..5b61acf1f3d 100644 --- a/app/controllers/idv/welcome_controller.rb +++ b/app/controllers/idv/welcome_controller.rb @@ -33,6 +33,15 @@ def update redirect_to idv_agreement_url end + def self.step_info + Idv::StepInfo.new( + key: :welcome, + controller: controller_name, + next_steps: [:agreement], + preconditions: ->(idv_session:, user:) { true }, + ) + end + private def analytics_arguments diff --git a/app/controllers/vendor_outage_controller.rb b/app/controllers/vendor_outage_controller.rb index cf353672a33..e49392e522f 100644 --- a/app/controllers/vendor_outage_controller.rb +++ b/app/controllers/vendor_outage_controller.rb @@ -16,6 +16,6 @@ def from_idv_phone? def gpo_letter_available? FeatureManagement.gpo_verification_enabled? && current_user && - !Idv::GpoMail.new(current_user).mail_spammed? + !Idv::GpoMail.new(current_user).rate_limited? end end diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index ac8a8f85b32..3e3470a4c70 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -111,8 +111,11 @@ def validate_pii_from_doc(client_response) attention_with_barcode: client_response.attention_with_barcode?, ).submit response.extra.merge!(extra_attributes) + side_classification = doc_side_classification(client_response) + response_with_classification = + response.to_h.merge(side_classification) - analytics.idv_doc_auth_submitted_pii_validation(**response.to_h) + analytics.idv_doc_auth_submitted_pii_validation(**response_with_classification) if client_response.success? && response.success? store_pii(client_response) @@ -121,6 +124,14 @@ def validate_pii_from_doc(client_response) response end + def doc_side_classification(client_response) + side_info = {}.merge(client_response&.extra&.[](:classification_info) || {}) + side_info.transform_keys(&:downcase).symbolize_keys + { + classification_info: side_info, + } + end + def extra_attributes return @extra_attributes if defined?(@extra_attributes) && @extra_attributes&.dig('attempts') == attempts @@ -290,7 +301,8 @@ def update_analytics(client_response:, vendor_request_time_in_ms:) async: false, flow_path: params[:flow_path], vendor_request_time_in_ms: vendor_request_time_in_ms, - ).merge(acuant_sdk_upgrade_ab_test_data). + ).except(:classification_info). + merge(acuant_sdk_upgrade_ab_test_data). merge(getting_started_ab_test_analytics_bucket). merge(phone_question_ab_test_analytics_bucket), ) diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index dc81220c050..44dc1a798eb 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -10,7 +10,6 @@ import { useI18n } from '@18f/identity-react-i18n'; import UnknownError from './unknown-error'; import TipList from './tip-list'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; -import DocumentCaptureAbandon from './document-capture-abandon'; interface DocumentCaptureReviewIssuesProps { isFailedDocType: boolean; @@ -79,7 +78,6 @@ function DocumentCaptureReviewIssues({ /> ))} - ); diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx index bad438701a6..ccb2763f600 100644 --- a/app/javascript/packages/document-capture/components/documents-step.jsx +++ b/app/javascript/packages/document-capture/components/documents-step.jsx @@ -8,7 +8,6 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import TipList from './tip-list'; -import DocumentCaptureAbandon from './document-capture-abandon'; /** * @typedef {'front'|'back'} DocumentSide @@ -72,7 +71,6 @@ function DocumentsStep({ ))} {isLastStep ? : } - ); diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index e09114044ba..f0c319b0b5e 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -4,6 +4,6 @@ "version": "1.0.0", "dependencies": { "intl-tel-input": "^17.0.19", - "libphonenumber-js": "^1.10.48" + "libphonenumber-js": "^1.10.49" } } diff --git a/app/jobs/reports/monthly_key_metrics_report.rb b/app/jobs/reports/monthly_key_metrics_report.rb index 463273051c9..9e346e358d6 100644 --- a/app/jobs/reports/monthly_key_metrics_report.rb +++ b/app/jobs/reports/monthly_key_metrics_report.rb @@ -1,5 +1,6 @@ require 'csv' require 'reporting/monthly_proofing_report' +require 'reporting/proofing_rate_report' module Reports class MonthlyKeyMetricsReport < BaseReport @@ -53,13 +54,12 @@ def reports active_users_count_report.active_users_count_emailable_report, # Total Annual Users - LG-11150 total_user_count_report.total_user_count_emailable_report, - # Proofing rate(s) (tbd on this one pager) - LG-11152 + proofing_rate_report.proofing_rate_emailable_report, account_deletion_rate_report.account_deletion_emailable_report, account_reuse_report.account_reuse_emailable_report, account_reuse_report.total_identities_emailable_report, monthly_proofing_report.document_upload_proofing_emailable_report, - # Number of applications using Login (separated by auth / IdV) - LG-11154 - # Number of agencies using Login - LG-11155 + agency_and_sp_report.agency_and_sp_emailable_report, # APG Reporting Annual Active Users by FY (w/ cumulative Active Users by quarter) - LG-11156 # APG Reporting of Active Federal Partner Agencies - LG-11157 # APG Reporting of Active Login.gov Serviced Applications - LG-11158 @@ -77,6 +77,10 @@ def emails emails end + def proofing_rate_report + @proofing_rate_report ||= Reporting::ProofingRateReport.new(end_date: report_date) + end + def account_reuse_report @account_reuse_report ||= Reporting::AccountReuseAndTotalIdentitiesReport.new(report_date) end @@ -104,6 +108,10 @@ def active_users_count_report ) end + def agency_and_sp_report + @agency_and_sp_report ||= Reporting::AgencyAndSpReport.new(report_date) + end + def upload_to_s3(report_body, report_name: nil) _latest, path = generate_s3_paths(REPORT_NAME, 'csv', subname: report_name, now: report_date) diff --git a/app/policies/idv/flow_policy.rb b/app/policies/idv/flow_policy.rb new file mode 100644 index 00000000000..0ced801a2c6 --- /dev/null +++ b/app/policies/idv/flow_policy.rb @@ -0,0 +1,62 @@ +module Idv + class FlowPolicy + attr_reader :idv_session, :user + + def initialize(idv_session:, user:) + @idv_session = idv_session + @user = user + end + + def controller_allowed?(controller:) + controller_name = controller.ancestors.include?(ApplicationController) ? + controller.controller_name : controller + key = controller_to_key(controller: controller_name) + step_allowed?(key: key) + end + + def info_for_latest_step + steps[latest_step] + end + + private + + def latest_step(current_step: :root) + return nil if steps[current_step]&.next_steps.blank? + return current_step if steps[current_step].next_steps == [:success] + + steps[current_step].next_steps.each do |key| + if step_allowed?(key: key) + return latest_step(current_step: key) + end + end + current_step + end + + def steps + { + root: Idv::StepInfo.new( + key: :root, + controller: AccountsController.controller_name, + next_steps: [:welcome], + preconditions: ->(idv_session:, user:) { true }, + ), + welcome: Idv::WelcomeController.step_info, + agreement: Idv::AgreementController.step_info, + phone_question: Idv::PhoneQuestionController.step_info, + hybrid_handoff: Idv::HybridHandoffController.step_info, + link_sent: Idv::LinkSentController.step_info, + document_capture: Idv::DocumentCaptureController.step_info, + } + end + + def step_allowed?(key:) + steps[key].preconditions.call(idv_session: idv_session, user: user) + end + + def controller_to_key(controller:) + steps.keys.each do |key| + return key if steps[key].controller == controller + end + end + end +end diff --git a/app/policies/idv/step_info.rb b/app/policies/idv/step_info.rb new file mode 100644 index 00000000000..c9b2ea332d7 --- /dev/null +++ b/app/policies/idv/step_info.rb @@ -0,0 +1,33 @@ +module Idv + class StepInfo + include ActiveModel::Validations + + attr_reader :key, :controller, :action, :next_steps, :preconditions + + validates :controller, presence: true + validates :action, presence: true + validate :next_steps_validation, :preconditions_validation + + def initialize(key:, controller:, next_steps:, preconditions:, action: :show) + @key = key + @controller = controller + @action = action + @next_steps = next_steps + @preconditions = preconditions + + raise ArgumentError unless valid? + end + + def next_steps_validation + unless next_steps.is_a?(Array) + errors.add(:next_steps, type: :invalid_argument, message: 'next_steps must be an Array') + end + end + + def preconditions_validation + unless preconditions.is_a?(Proc) + errors.add(:preconditions, type: :invalid_argument, message: 'preconditions must be a Proc') + end + end + end +end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 782d96bb60a..2c14cdbf9cd 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -882,6 +882,7 @@ def idv_doc_auth_submitted_image_upload_vendor( # @param [String] front_image_fingerprint Fingerprint of front image data # @param [String] back_image_fingerprint Fingerprint of back image data # @param [String] getting_started_ab_test_bucket Which initial IdV screen the user saw + # @param [Hash] classification_info document image side information, issuing country and type etc # The PII that came back from the document capture vendor was validated def idv_doc_auth_submitted_pii_validation( success:, @@ -893,6 +894,7 @@ def idv_doc_auth_submitted_pii_validation( front_image_fingerprint: nil, back_image_fingerprint: nil, getting_started_ab_test_bucket: nil, + classification_info: {}, **extra ) track_event( @@ -906,6 +908,7 @@ def idv_doc_auth_submitted_pii_validation( front_image_fingerprint: front_image_fingerprint, back_image_fingerprint: back_image_fingerprint, getting_started_ab_test_bucket: getting_started_ab_test_bucket, + classification_info: classification_info, **extra, ) end diff --git a/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb b/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb index d14be08e21d..e4253ca4caa 100644 --- a/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb +++ b/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb @@ -12,18 +12,6 @@ def create_document raise NotImplementedError end - def post_front_image(image:, instance_id: nil) - raise NotImplementedError - end - - def post_back_image(image:, instance_id: nil) - raise NotImplementedError - end - - def get_results(instance_id:) - raise NotImplementedError - end - def post_images( front_image:, back_image:, diff --git a/app/services/doc_auth/lexis_nexis/responses/lexis_nexis_response.rb b/app/services/doc_auth/lexis_nexis/responses/lexis_nexis_response.rb deleted file mode 100644 index 6ecc40949b8..00000000000 --- a/app/services/doc_auth/lexis_nexis/responses/lexis_nexis_response.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -module DocAuth - module LexisNexis - module Responses - class LexisNexisResponse < DocAuth::Response - attr_reader :http_response - - def initialize(http_response) - @http_response = http_response - - super( - success: successful_result?, - errors: error_messages, - extra: extra_attributes, - pii_from_doc: pii_from_doc, - ) - rescue StandardError => e - NewRelic::Agent.notice_error(e) - super( - success: false, - errors: { network: true }, - exception: e, - extra: { backtrace: e.backtrace }, - ) - end - - def successful_result? - raise NotImplementedError - end - - def error_messages - raise NotImplementedError - end - - def extra_attributes - raise NotImplementedError - end - - def pii_from_doc - raise NotImplementedError - end - - private - - def parsed_response_body - @parsed_response_body ||= JSON.parse(http_response.body).with_indifferent_access - end - - def transaction_status_passed? - transaction_status == 'passed' - end - - def transaction_status - parsed_response_body.dig(:Status, :TransactionStatus) - end - - def transaction_reason_code - @transaction_reason_code ||= - parsed_response_body.dig(:Status, :TransactionReasonCode, :Code) - end - - def conversation_id - @conversation_id ||= parsed_response_body.dig(:Status, :ConversationId) - end - - def reference - @reference ||= parsed_response_body.dig(:Status, :Reference) - end - - def products - @products ||= - parsed_response_body.dig(:Products)&.each_with_object({}) do |product, product_list| - extract_details(product) - product_list[product[:ProductType]] = product - end&.with_indifferent_access - end - - def extract_details(product) - return unless product[:ParameterDetails] - - product[:ParameterDetails].each do |detail| - group = detail[:Group] - detail_name = detail[:Name] - is_region = detail_name.end_with?('Regions', 'Regions_Reference') - value = is_region ? detail[:Values].map { |v| v[:Value] } : - detail.dig(:Values, 0, :Value) - product[group] ||= {} - - product[group][detail_name] = value - end - end - end - end - end -end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index ac7aaf933aa..731bf9961b3 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -3,7 +3,7 @@ module DocAuth module LexisNexis module Responses - class TrueIdResponse < LexisNexisResponse + class TrueIdResponse < DocAuth::Response include ClassificationConcern PII_EXCLUDES = %w[ Age @@ -42,12 +42,26 @@ class TrueIdResponse < LexisNexisResponse 'Fields_DocumentClassName' => :state_id_type, 'Fields_CountryCode' => :issuing_country_code, }.freeze - attr_reader :config + attr_reader :config, :http_response def initialize(http_response, config) @config = config + @http_response = http_response - super http_response + super( + success: successful_result?, + errors: error_messages, + extra: extra_attributes, + pii_from_doc: pii_from_doc, + ) + rescue StandardError => e + NewRelic::Agent.notice_error(e) + super( + success: false, + errors: { network: true }, + exception: e, + extra: { backtrace: e.backtrace }, + ) end def successful_result? @@ -129,6 +143,54 @@ def billed? private + def conversation_id + @conversation_id ||= parsed_response_body.dig(:Status, :ConversationId) + end + + def parsed_response_body + @parsed_response_body ||= JSON.parse(http_response.body).with_indifferent_access + end + + def transaction_status + parsed_response_body.dig(:Status, :TransactionStatus) + end + + def transaction_status_passed? + transaction_status == 'passed' + end + + def transaction_reason_code + @transaction_reason_code ||= + parsed_response_body.dig(:Status, :TransactionReasonCode, :Code) + end + + def reference + @reference ||= parsed_response_body.dig(:Status, :Reference) + end + + def products + @products ||= + parsed_response_body.dig(:Products)&.each_with_object({}) do |product, product_list| + extract_details(product) + product_list[product[:ProductType]] = product + end&.with_indifferent_access + end + + def extract_details(product) + return unless product[:ParameterDetails] + + product[:ParameterDetails].each do |detail| + group = detail[:Group] + detail_name = detail[:Name] + is_region = detail_name.end_with?('Regions', 'Regions_Reference') + value = is_region ? detail[:Values].map { |v| v[:Value] } : + detail.dig(:Values, 0, :Value) + product[group] ||= {} + + product[group][detail_name] = value + end + end + def response_info @response_info ||= create_response_info end @@ -181,10 +243,6 @@ def doc_auth_result_attention? doc_auth_result == 'Attention' end - def doc_auth_result_unknown? - doc_auth_result == 'Unknown' - end - def doc_class_name true_id_product&.dig(:AUTHENTICATION_RESULT, :DocClassName) end diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index 92c734940e7..e3cb88ad448 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -17,6 +17,7 @@ def initialize(uploaded_file, config) extra: { doc_auth_result: doc_auth_result, billed: true, + classification_info: classification_info, }, ) end @@ -29,14 +30,17 @@ def errors {} else doc_auth_result = file_data.dig('doc_auth_result') + # Error generator is not to be called when it's not failure + # allows us to test successful results + return {} if doc_auth_result == 'Passed' image_metrics = file_data.dig('image_metrics') failed = file_data.dig('failed_alerts') passed = file_data.dig('passed_alerts') liveness_result = file_data.dig('liveness_result') classification_info = file_data.dig('classification_info') - if [doc_auth_result, image_metrics, failed, passed, liveness_result, - classification_info].any?(&:present?) + if [doc_auth_result, image_metrics, failed, passed, + liveness_result, classification_info].any?(&:present?) mock_args = {} mock_args[:doc_auth_result] = doc_auth_result if doc_auth_result.present? mock_args[:image_metrics] = image_metrics.symbolize_keys if image_metrics.present? @@ -46,7 +50,6 @@ def errors mock_args[:classification_info] = classification_info if classification_info.present? fake_response_info = create_response_info(**mock_args) - ErrorGenerator.new(config).generate_doc_auth_errors(fake_response_info) elsif file_data.include?(:general) # general is the key for errors from parsing file_data @@ -134,7 +137,8 @@ def doc_auth_result_from_uploaded_file end def classification_info - parsed_data_from_uploaded_file&.[]('classification_info') + info = parsed_data_from_uploaded_file&.[]('classification_info') || {} + info.to_h.symbolize_keys end def doc_auth_result_from_success diff --git a/app/services/idv/gpo_mail.rb b/app/services/idv/gpo_mail.rb index 6db50371a13..569c6447f8e 100644 --- a/app/services/idv/gpo_mail.rb +++ b/app/services/idv/gpo_mail.rb @@ -4,7 +4,7 @@ def initialize(current_user) @current_user = current_user end - def mail_spammed? + def rate_limited? too_many_letter_requests_within_window? || last_letter_request_too_recent? end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index e5c348fbd35..8520439def6 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -15,6 +15,7 @@ class Session mail_only_warning_shown personal_key phone_for_mobile_flow + phone_with_camera pii pii_from_doc previous_phone_step_params @@ -123,6 +124,10 @@ def create_gpo_entry @gpo_otp = confirmation_maker.otp end + def phone_otp_sent? + user_phone_confirmation_session.present? + end + def user_phone_confirmation_session session_value = session[:user_phone_confirmation_session] return if session_value.blank? diff --git a/app/services/idv/steps/in_person/state_id_step.rb b/app/services/idv/steps/in_person/state_id_step.rb index c3079c55468..ac2a68e62b4 100644 --- a/app/services/idv/steps/in_person/state_id_step.rb +++ b/app/services/idv/steps/in_person/state_id_step.rb @@ -38,6 +38,11 @@ def call if flow_session['Idv::Steps::InPerson::AddressStep'] redirect_to idv_in_person_verify_info_url end + + if pii_from_user[:same_address_as_id] == 'false' && + IdentityConfig.store.in_person_residential_address_controller_enabled + redirect_to idv_in_person_proofing_address_url + end end def extra_view_variables diff --git a/app/services/reporting/agency_and_sp_report.rb b/app/services/reporting/agency_and_sp_report.rb new file mode 100644 index 00000000000..c94aa3bbd6b --- /dev/null +++ b/app/services/reporting/agency_and_sp_report.rb @@ -0,0 +1,39 @@ +module Reporting + class AgencyAndSpReport + attr_reader :report_date + + def initialize(report_date = Time.zone.today) + @report_date = report_date + end + + def agency_and_sp_report + idv_sps, auth_sps = ServiceProvider.where('created_at <= ?', report_date).active. + partition { |sp| sp.ial.present? && sp.ial >= 2 } + idv_agency_ids = idv_sps.map(&:agency_id).uniq + idv_agencies, auth_agencies = agencies_with_sps.partition do |agency| + idv_agency_ids.include?(agency.id) + end + + [ + ['', 'Number of apps (SPs)', 'Number of agencies'], + ['Auth', auth_sps.count, auth_agencies.count], + ['IDV', idv_sps.count, idv_agencies.count], + ] + end + + def agency_and_sp_emailable_report + EmailableReport.new( + title: 'App and Agency Counts', + table: agency_and_sp_report, + filename: 'agency_and_sp_counts', + ) + end + + # Agencies have no timestamps, so we need to join to SPs to get something equivalent. + def agencies_with_sps + Agency.joins(:service_providers). + where('service_providers.created_at <= ?', report_date). + distinct + end + end +end diff --git a/app/views/account_reset/pending/cancel.html.erb b/app/views/account_reset/pending/cancel.html.erb index 63ebb0f7831..f87a8e46ec9 100644 --- a/app/views/account_reset/pending/cancel.html.erb +++ b/app/views/account_reset/pending/cancel.html.erb @@ -1,3 +1,5 @@ +<% title t('account_reset.pending.cancelled') %> + <%= render PageHeadingComponent.new.with_content(t('account_reset.pending.cancelled')) %> <%= link_to( diff --git a/app/views/account_reset/pending/confirm.html.erb b/app/views/account_reset/pending/confirm.html.erb index 872066ff582..4811999f91c 100644 --- a/app/views/account_reset/pending/confirm.html.erb +++ b/app/views/account_reset/pending/confirm.html.erb @@ -1,3 +1,5 @@ +<% title t('account_reset.cancel_request.title') %> +

<%= t('account_reset.pending.confirm') %>

<%= button_to( diff --git a/app/views/idv/by_mail/enter_code/index.html.erb b/app/views/idv/by_mail/enter_code/index.html.erb index 1c62bc8500f..f36d527404c 100644 --- a/app/views/idv/by_mail/enter_code/index.html.erb +++ b/app/views/idv/by_mail/enter_code/index.html.erb @@ -16,7 +16,7 @@ <% if !@can_request_another_letter %> <%= render AlertComponent.new(type: :warning, class: 'margin-bottom-4') do %> <%= t( - 'idv.gpo.alert_spam_warning_html', + 'idv.gpo.alert_rate_limit_warning_html', date_letter_was_sent: I18n.l( @last_date_letter_was_sent, format: :event_date, diff --git a/app/views/idv/in_person/address/show.html.erb b/app/views/idv/in_person/address/show.html.erb new file mode 100644 index 00000000000..61f542644be --- /dev/null +++ b/app/views/idv/in_person/address/show.html.erb @@ -0,0 +1,95 @@ +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS, + current_step: :verify_info, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> +<% end %> + +<% if updating_address %> + <% title t('in_person_proofing.headings.update_address') %> + <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.update_address')) %> +<% else %> + <% title t('in_person_proofing.headings.address') %> + <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.address')) %> +<% end %> + +<%= simple_form_for( + form, url: url_for, method: 'PUT', + html: { autocomplete: 'off' } + ) do |f| %> + <%= render ValidatedFieldComponent.new( + collection: us_states_territories, + form: f, + input_html: { class: 'address-state-selector' }, + label: t('idv.form.state'), + label_html: { class: 'usa-label' }, + name: :state, + prompt: t('in_person_proofing.form.address.state_prompt'), + required: true, + selected: pii[:state], + ) %> + <%= render ValidatedFieldComponent.new( + form: f, + hint: t('in_person_proofing.form.state_id.address1_hint'), + hint_html: { class: ['display-none', 'puerto-rico-extras'] }, + input_html: { value: pii[:address1] }, + label: t('idv.form.address1'), + label_html: { class: 'usa-label' }, + maxlength: 255, + name: :address1, + required: true, + ) %> + <%= render ValidatedFieldComponent.new( + form: f, + hint: t('in_person_proofing.form.state_id.address2_hint'), + hint_html: { class: ['display-none', 'puerto-rico-extras'] }, + input_html: { value: pii[:address2] }, + label: t('idv.form.address2'), + label_html: { class: 'usa-label' }, + maxlength: 255, + name: :address2, + required: false, + ) %> + <%= render ValidatedFieldComponent.new( + form: f, + input_html: { value: pii[:city] }, + label: t('idv.form.city'), + label_html: { class: 'usa-label' }, + maxlength: 255, + name: :city, + required: true, + ) %> + +
+ <%# using :tel for mobile numeric keypad %> + <%= render ValidatedFieldComponent.new( + as: :tel, + error_messages: { patternMismatch: t('idv.errors.pattern_mismatch.zipcode') }, + form: f, + input_html: { value: pii[:zipcode], class: 'zipcode' }, + label: t('idv.form.zipcode'), + label_html: { class: 'usa-label' }, + name: :zipcode, + pattern: '\d{5}([\-]\d{4})?', + required: true, + ) %> +
+ + <%= f.submit class: 'margin-top-1' do %> + <% if updating_address %> + <%= t('forms.buttons.submit.update') %> + <% else %> + <%= t('forms.buttons.continue') %> + <% end %> + <% end %> +<% end %> + +<% if updating_address %> + <%= render 'idv/shared/back', action: 'cancel_update_address' %> +<% else %> + <%= render 'idv/doc_auth/cancel', step: 'address' %> +<% end %> + +<%= javascript_packs_tag_once('formatted-fields', 'puerto-rico-guidance') %> diff --git a/app/views/idv/mail_only_warning/show.html.erb b/app/views/idv/mail_only_warning/show.html.erb index 210a4b79233..aa7e4c8961c 100644 --- a/app/views/idv/mail_only_warning/show.html.erb +++ b/app/views/idv/mail_only_warning/show.html.erb @@ -1,3 +1,5 @@ +<% title t('vendor_outage.alerts.pinpoint.idv.header') %> + <%= render StepIndicatorComponent.new( steps: step_indicator_steps, current_step: :getting_started, diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index a103815d8f8..dec76c479c2 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -15,10 +15,7 @@ <% end %> - <%= content_tag( - 'title', - content_for?(:title) ? raw("#{yield(:title)} | #{APP_NAME}") : APP_NAME, - ) %> + <%= yield(:title).presence || (raise 'Missing title') %> | <%= APP_NAME %> <%= javascript_tag(nonce: true) do %> document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js'); diff --git a/app/views/saml_idp/auth/error.html.erb b/app/views/saml_idp/auth/error.html.erb index e80a300a4d9..8122f125745 100644 --- a/app/views/saml_idp/auth/error.html.erb +++ b/app/views/saml_idp/auth/error.html.erb @@ -1,3 +1,5 @@ +<% title t('saml_idp.auth.error.title') %> +
    <% @saml_request_validator.errors.full_messages.each do |message| %>
  • <%= message %>
  • diff --git a/app/views/saml_idp/shared/saml_post_binding.html.erb b/app/views/saml_idp/shared/saml_post_binding.html.erb index 7a856da2f28..fad016f2439 100644 --- a/app/views/saml_idp/shared/saml_post_binding.html.erb +++ b/app/views/saml_idp/shared/saml_post_binding.html.erb @@ -2,6 +2,7 @@ + <%= t('.redirecting') %> | <%= APP_NAME %> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= render_stylesheet_once_tags %> diff --git a/app/views/test/push_notification/index.html.erb b/app/views/test/push_notification/index.html.erb index 0677829a26d..0f086f97eaf 100644 --- a/app/views/test/push_notification/index.html.erb +++ b/app/views/test/push_notification/index.html.erb @@ -1,3 +1,5 @@ +<% title 'Push Notification Events' %> + <%= content_for(:meta_refresh) { '15' } %>

    Push Notification Events

    diff --git a/app/views/test/saml_test/decode_response.html.erb b/app/views/test/saml_test/decode_response.html.erb index ec426e239e2..1e235b9e7c5 100644 --- a/app/views/test/saml_test/decode_response.html.erb +++ b/app/views/test/saml_test/decode_response.html.erb @@ -1,3 +1,5 @@ +<% title 'Decoded SAML Response' %> +

    Decoded SAML Response

    diff --git a/app/views/test/telephony/index.html.erb b/app/views/test/telephony/index.html.erb index d77cdeb90f8..d296cae7056 100644 --- a/app/views/test/telephony/index.html.erb +++ b/app/views/test/telephony/index.html.erb @@ -1,3 +1,5 @@ +<% title 'Outbound calls and messages' %> + <%= content_for(:meta_refresh) { '15' } %>

    Outbound calls and messages

    diff --git a/app/views/users/verify_personal_key/new.html.erb b/app/views/users/verify_personal_key/new.html.erb index 4929b1f3fb2..657ffda239b 100644 --- a/app/views/users/verify_personal_key/new.html.erb +++ b/app/views/users/verify_personal_key/new.html.erb @@ -1,3 +1,5 @@ +<% title t('forms.personal_key.title') %> + <%= simple_form_for('', url: create_verify_personal_key_path, method: 'post', html: { class: 'margin-top-8' }) do |f| %>
    <%= render AlertIconComponent.new( diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 85bcb27e688..264b53d5946 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -42,7 +42,7 @@ en: messages: already_confirmed: was already confirmed, please try signing in blank: Please fill in this field. - confirmation_code_incorrect: Incorrect code. Did you type it in correctly? + confirmation_code_incorrect: Incorrect verification code. Did you type it in correctly? confirmation_invalid_token: Invalid confirmation link. Either the link expired or you already confirmed your account. confirmation_period_expired: Expired confirmation link. You can click “Resend diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index 474c0d9f4a8..601b07583f8 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -45,7 +45,7 @@ es: messages: already_confirmed: ya estaba confirmado, por favor intente iniciar una sesión blank: Por favor, rellenar este campo. - confirmation_code_incorrect: El código es incorrecto. ¿Lo escribió correctamente? + confirmation_code_incorrect: Código de verificación incorrecto. ¿Lo escribió correctamente? confirmation_invalid_token: El enlace de confirmación no es válido. El enlace expiró o usted ya ha confirmado su cuenta. confirmation_period_expired: El enlace de confirmación expiró. Puede hacer clic diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index d6e9a35538e..f1680798ac0 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -50,7 +50,7 @@ fr: messages: already_confirmed: a déjà été confirmé, veuillez essayer de vous connecter blank: Veuillez remplir ce champ. - confirmation_code_incorrect: Code non valide. L’avez-vous inscrit correctement? + confirmation_code_incorrect: Code de vérification incorrect. L’avez-vous saisi correctement ? confirmation_invalid_token: Lien de confirmation non valide. Le lien est expiré ou vous avez déjà confirmé votre compte. confirmation_period_expired: Lien de confirmation expiré. Vous pouvez cliquer diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index f897ed0c315..46cff53b0d0 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -175,8 +175,8 @@ en: JavaScript to continue this process.' gpo: alert_info: 'We sent a letter with your verification code to:' - alert_spam_warning_html: You can’t request more letters right now. Your previous - letter request was on %{date_letter_was_sent}. + alert_rate_limit_warning_html: You can’t request more letters right now. Your + previous letter request was on %{date_letter_was_sent}. change_to_verification_code_html: 'The one-time code from your letter is now referred to as verification code.' clear_and_start_over: Clear your information and start over diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 984d2a801e0..c7dbcd8caff 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -184,8 +184,9 @@ es: habilitar JavaScript para continuar con este proceso.' gpo: alert_info: 'Enviamos una carta con su código de verificación a:' - alert_spam_warning_html: No puede solicitar más cartas ahora mismo. Su solicitud - de carta anterior la hizo el %{date_letter_was_sent}. + alert_rate_limit_warning_html: No puede solicitar más cartas ahora mismo. Su + solicitud de carta anterior la hizo el + %{date_letter_was_sent}. change_to_verification_code_html: 'El código único de su carta ahora se conoce como código de verificación.' clear_and_start_over: Borrar su información y empezar de nuevo diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 7bdc2a1bb1b..8d71e145d6d 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -189,8 +189,8 @@ fr: devez activer JavaScript pour poursuivre ce processus.' gpo: alert_info: 'Nous avons envoyé une lettre avec votre code de vérification à:' - alert_spam_warning_html: Vous ne pouvez pas demander d’autres lettres pour le - moment. Votre précédente demande de lettre a été effectuée le + alert_rate_limit_warning_html: Vous ne pouvez pas demander d’autres lettres pour + le moment. Votre précédente demande de lettre a été effectuée le %{date_letter_was_sent}. change_to_verification_code_html: 'Le code à usage unique figurant dans votre lettre est désormais appelé code de diff --git a/config/locales/saml_idp/en.yml b/config/locales/saml_idp/en.yml index 512497f186b..f273cd0af06 100644 --- a/config/locales/saml_idp/en.yml +++ b/config/locales/saml_idp/en.yml @@ -1,9 +1,13 @@ --- en: saml_idp: + auth: + error: + title: Error shared: saml_post_binding: heading: Submit to continue no_js: JavaScript seems to be turned off in your browser. Normally this step happens automatically, but because you have JavaScript turned off, please click the submit button to continue signing in or signing out. + redirecting: Redirecting diff --git a/config/locales/saml_idp/es.yml b/config/locales/saml_idp/es.yml index fcff2599f47..9e57fd4c088 100644 --- a/config/locales/saml_idp/es.yml +++ b/config/locales/saml_idp/es.yml @@ -1,6 +1,9 @@ --- es: saml_idp: + auth: + error: + title: Error shared: saml_post_binding: heading: Enviar para continuar @@ -8,3 +11,4 @@ es: paso se realiza automáticamente, pero debido a que tiene JavaScript desactivado, haga clic en el botón Enviar para continuar iniciando o cerrando la sesión. + redirecting: Redirigiendo diff --git a/config/locales/saml_idp/fr.yml b/config/locales/saml_idp/fr.yml index 0a1051b49c3..4b31787ed91 100644 --- a/config/locales/saml_idp/fr.yml +++ b/config/locales/saml_idp/fr.yml @@ -1,6 +1,9 @@ --- fr: saml_idp: + auth: + error: + title: Erreur shared: saml_post_binding: heading: Soumettre pour continuer @@ -8,3 +11,4 @@ fr: cette étape se déroule automatiquement, mais parce que vous avez désactivé le JavaScript, veuillez cliquer sur le lien « soumettre » pour continuer ou pour vous déconnecter. + redirecting: Redirection diff --git a/config/routes.rb b/config/routes.rb index 99bf4c658f5..4c2d4376057 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -348,9 +348,6 @@ post '/phone/resend_code' => 'resend_otp#create', as: :resend_otp get '/phone_confirmation' => 'otp_verification#show', as: :otp_verification put '/phone_confirmation' => 'otp_verification#update', as: :nil - # The `/review` route is deperecated in favor of `/enter_password` - get '/review' => 'enter_password#new' - put '/review' => 'enter_password#create' get '/enter_password' => 'enter_password#new' put '/enter_password' => 'enter_password#create' get '/phone_question' => 'phone_question#show' @@ -377,14 +374,7 @@ # sometimes underscores get messed up when linked to via SMS as: :capture_doc_dashes - # DEPRECATION NOTICE - # Usage of the /in_person_proofing/ssn routes is deprecated. - # Use the /in_person/ssn routes instead. - # - # These have been left in temporarily to prevent any impact to users - # during the deprecation process. - get '/in_person_proofing/ssn' => redirect('/verify/in_person/ssn', status: 307) - put '/in_person_proofing/ssn' => redirect('/verify/in_person/ssn', status: 307) + get '/in_person_proofing/address' => 'in_person/address#show' get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', diff --git a/lib/base16.rb b/lib/base16.rb deleted file mode 100644 index 6ffc5fa64c4..00000000000 --- a/lib/base16.rb +++ /dev/null @@ -1,10 +0,0 @@ -# See https://www.rfc-editor.org/rfc/rfc4648#section-8 -class Base16 - def self.encode16(str) - str.unpack1('H*').tap(&:upcase!) - end - - def self.decode16(str) - [str].pack('H*') - end -end diff --git a/lib/cleanup/destroyable_records.rb b/lib/cleanup/destroyable_records.rb index 833b560bba8..08cf370a4f4 100644 --- a/lib/cleanup/destroyable_records.rb +++ b/lib/cleanup/destroyable_records.rb @@ -15,7 +15,7 @@ def initialize(issuer, stdin: STDIN, stdout: STDOUT) def print_data stdout.puts "You are about to delete a service provider with issuer #{service_provider.issuer}" - if integration.partner_account.present? + if integration&.partner_account.present? stdout.puts "The partner is #{integration.partner_account.name}" end stdout.puts "\n\n" @@ -26,7 +26,11 @@ def print_data stdout.puts '********' stdout.puts 'Integration:' - stdout.puts integration.attributes.to_yaml + if integration.nil? + stdout.puts 'No associated integration' + else + stdout.puts integration.attributes.to_yaml + end stdout.puts "\n" stdout.puts '********' @@ -42,8 +46,13 @@ def print_data stdout.puts '*******' stdout.puts 'These are the IAA orders that will be affected: \n' - iaa_orders.each do |iaa_order| - stdout.puts "#{iaa_order.iaa_gtc.gtc_number} Order #{iaa_order.order_number}" + if iaa_orders.nil? + stdout.puts 'No IAA orders will be affected' + else + stdout.puts 'These are the IAA orders that will be affected: \n' + iaa_orders.each do |iaa_order| + stdout.puts "#{iaa_order.iaa_gtc.gtc_number} Order #{iaa_order.order_number}" + end end stdout.puts "\n" end @@ -70,11 +79,11 @@ def destroy_records private def integration_usages - integration.integration_usages + integration&.integration_usages end def iaa_orders - integration.iaa_orders + integration&.iaa_orders end def in_person_enrollments diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index c8a0d5c1145..4fdb59f44ec 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -103,6 +103,24 @@ module Vendors same_address_as_id: nil, }.freeze + MOCK_IPP_APPLICANT = { + first_name: 'FAKEY', + last_name: 'MCFAKERSON', + dob: '1938-10-06', + identity_doc_address1: '123 Way St', + identity_doc_address2: '2nd Address Line', + identity_doc_city: 'Best City', + identity_doc_zipcode: '12345', + state_id_jurisdiction: 'Virginia', + identity_doc_address_state: 'VA', + state_id_number: '1111111111111', + same_address_as_id: 'true', + }.freeze + + MOCK_IPP_APPLICANT_SAME_ADDRESS_AS_ID_FALSE = MOCK_IPP_APPLICANT.merge( + same_address_as_id: 'false', + ).freeze + MOCK_IDV_APPLICANT_WITH_SSN = MOCK_IDV_APPLICANT.merge(ssn: '900-66-1234').freeze MOCK_IDV_APPLICANT_STATE_ID_ADDRESS = MOCK_IDV_APPLICANT_WITH_SSN.merge( diff --git a/lib/reporting/cloudwatch_client.rb b/lib/reporting/cloudwatch_client.rb index 4140e8949a6..4aedd855600 100644 --- a/lib/reporting/cloudwatch_client.rb +++ b/lib/reporting/cloudwatch_client.rb @@ -163,16 +163,19 @@ def fetch_one(query:, start_time:, end_time:) ).query_id wait_for_query_result(query_id) - rescue Aws::CloudWatchLogs::Errors::InvalidParameterException => err + # rubocop:disable Layout/LineLength, Rails/TimeZone + rescue Aws::CloudWatchLogs::Errors::InvalidParameterException, Aws::CloudWatchLogs::Errors::MalformedQueryException => err if err.message.match?(/End time should not be before the service was generally available/) - # rubocop:disable Layout/LineLength, Rails/TimeZone log(:warn, "query end_time=#{end_time} (#{Time.at(end_time)}) is before Cloudwatch Insights availability, skipping") - # rubocop:enable Layout/LineLength, Rails/TimeZone + Aws::CloudWatchLogs::Types::GetQueryResultsResponse.new(results: []) + elsif err.message.match?(/end date and time is either before the log groups creation time or exceeds the log groups log retention settings/) + log(:warn, "query end_time=#{end_time} (#{Time.at(end_time)}) is before the log groups creation time or exceeds the log groups log retention settings") Aws::CloudWatchLogs::Types::GetQueryResultsResponse.new(results: []) else raise err end end + # rubocop:enable Layout/LineLength, Rails/TimeZone def ensure_complete_logs? @ensure_complete_logs diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb index 30960dd1fda..c63f26b63e1 100644 --- a/lib/reporting/identity_verification_report.rb +++ b/lib/reporting/identity_verification_report.rb @@ -54,7 +54,8 @@ def initialize( progress: false, slice: 3.hours, threads: 5, - data: nil + data: nil, + cloudwatch_client: nil ) @issuers = issuers @time_range = time_range @@ -63,6 +64,7 @@ def initialize( @slice = slice @threads = threads @data = data + @cloudwatch_client = cloudwatch_client end def verbose? diff --git a/lib/reporting/monthly_proofing_report.rb b/lib/reporting/monthly_proofing_report.rb index fefebbcd7d4..4b9a1de8bcf 100644 --- a/lib/reporting/monthly_proofing_report.rb +++ b/lib/reporting/monthly_proofing_report.rb @@ -56,6 +56,8 @@ def progress? def document_upload_proofing_emailable_report EmailableReport.new( title: 'Document upload proofing rates', + float_as_percent: true, + precision: 4, table: proofing_report, filename: 'document_upload_proofing', ) @@ -78,11 +80,6 @@ def proofing_report end csv - rescue Aws::CloudWatchLogs::Errors::MalformedQueryException => error - [ - ['Error', 'Message'], - [error.class.name, error.message], - ] end def as_csv diff --git a/lib/reporting/proofing_rate_report.rb b/lib/reporting/proofing_rate_report.rb index cad7b303c04..54a8f662000 100644 --- a/lib/reporting/proofing_rate_report.rb +++ b/lib/reporting/proofing_rate_report.rb @@ -24,14 +24,24 @@ def progress? @progress end + def proofing_rate_emailable_report + EmailableReport.new( + title: 'Proofing Rate Metrics', + float_as_percent: true, + precision: 2, + table: as_csv, + filename: 'proofing_rate_metrics', + ) + end + # rubocop:disable Layout/LineLength def as_csv csv = [] csv << ['Metric', *DATE_INTERVALS.map { |days| "Trailing #{days}d" }] - csv << ['Start Date', *reports.map(&:time_range).map(&:begin)] - csv << ['End Date', *reports.map(&:time_range).map(&:end)] + csv << ['Start Date', *reports.map(&:time_range).map(&:begin).map(&:to_date)] + csv << ['End Date', *reports.map(&:time_range).map(&:end).map(&:to_date)] csv << ['IDV Started', *reports.map(&:idv_started)] csv << ['Welcome Submitted', *reports.map(&:idv_doc_auth_welcome_submitted)] @@ -66,8 +76,7 @@ def reports (end_date - slice_start.days).beginning_of_day, (end_date - slice_end.days).beginning_of_day, ), - progress: false, - verbose: verbose?, + cloudwatch_client: cloudwatch_client, ).tap(&:data) end end @@ -119,6 +128,14 @@ def industry_proofing_rates(reports) ) end end + + def cloudwatch_client + @cloudwatch_client ||= Reporting::CloudwatchClient.new( + ensure_complete_logs: true, + progress: false, + logger: verbose? ? Logger.new(STDERR) : nil, + ) + end end end diff --git a/scripts/changelog_check.rb b/scripts/changelog_check.rb index e096ee3bd83..7388cb512cd 100755 --- a/scripts/changelog_check.rb +++ b/scripts/changelog_check.rb @@ -6,7 +6,6 @@ %r{^(?:\* )?changelog: ?(?[\w -]{2,}), ?(?[\w -]{2,}), ?(?.+)$}i CATEGORIES = [ 'User-Facing Improvements', - 'Improvements', # Temporary for transitional period 'Bug Fixes', 'Internal', 'Upcoming Features', @@ -106,7 +105,7 @@ def generate_invalid_changes(git_log) end def closest_change_category(change) - category = CATEGORIES. + CATEGORIES. map do |category| CategoryDistance.new( category, @@ -116,10 +115,6 @@ def closest_change_category(change) filter { |category_distance| category_distance.distance <= MAX_CATEGORY_DISTANCE }. max { |category_distance| category_distance.distance }&. category - - # Temporarily normalize legacy category in transitional period - category = 'User-Facing Improvements' if category == 'Improvements' - category end # Get the last valid changelog line for every Pull Request and tie it to the commit subject. diff --git a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb index f14e4bc47b3..001d92543b9 100644 --- a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb +++ b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb @@ -13,6 +13,7 @@ let(:acuant_sdk_args) { { as_bucket: :as_value } } let(:getting_started_args) { { gs_bucket: :gs_value } } + let(:phone_question_args) { { pq_bucket: :pq_value } } before do allow(subject).to receive(:current_user).and_return(user) @@ -20,12 +21,14 @@ and_return(acuant_sdk_args) expect(subject).to receive(:getting_started_ab_test_analytics_bucket). and_return(getting_started_args) + expect(subject).to receive(:phone_question_ab_test_analytics_bucket). + and_return(phone_question_args) end context 'idv_session is available' do before do sign_in(user) - expect(subject).to receive(:idv_session).and_return(idv_session) + expect(subject).to receive(:idv_session).twice.and_return(idv_session) end it 'includes acuant_sdk_ab_test_analytics_args' do expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args) @@ -39,6 +42,11 @@ idv_session.skip_hybrid_handoff = :shh_value expect(controller.ab_test_analytics_buckets).to include({ skip_hybrid_handoff: :shh_value }) end + + it 'includes phone_with_camera' do + idv_session.phone_with_camera = :the_value + expect(controller.ab_test_analytics_buckets).to include({ phone_with_camera: :the_value }) + end end context 'idv_session is not available' do diff --git a/spec/controllers/concerns/idv/phone_question_ab_test_concern_spec.rb b/spec/controllers/concerns/idv/phone_question_ab_test_concern_spec.rb index 98791019ec5..29ba28fb330 100644 --- a/spec/controllers/concerns/idv/phone_question_ab_test_concern_spec.rb +++ b/spec/controllers/concerns/idv/phone_question_ab_test_concern_spec.rb @@ -68,11 +68,17 @@ def index before do sign_in(user) end - + let(:visited) { nil } context 'A/B test specifies phone question page' do before do allow(controller).to receive(:phone_question_ab_test_bucket). and_return(:show_phone_question) + + idv_session = instance_double(Idv::Session) + allow(idv_session).to receive(:method_missing). + with(:phone_with_camera). + and_return(visited) + allow(controller).to receive(:idv_session).and_return(idv_session) end it 'redirects to idv_phone_question_url' do @@ -82,10 +88,7 @@ def index end context 'referred from phone question page' do - let(:referer) { idv_phone_question_url } - before do - request.env['HTTP_REFERER'] = referer - end + let(:visited) { true } it 'does not redirect users away from hybrid handoff page' do get :index diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb index a7045c4f691..94308829e26 100644 --- a/spec/controllers/idv/agreement_controller_spec.rb +++ b/spec/controllers/idv/agreement_controller_spec.rb @@ -14,6 +14,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::AgreementController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( diff --git a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb index 13c6f620675..f25cfd85661 100644 --- a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb +++ b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb @@ -19,7 +19,7 @@ :before, :confirm_two_factor_authenticated, :confirm_idv_needed, - :confirm_mail_not_spammed, + :confirm_mail_not_rate_limited, :confirm_profile_not_too_old, ) end @@ -54,7 +54,7 @@ end it 'redirects if the user has sent too much mail' do - allow(controller.gpo_mail_service).to receive(:mail_spammed?).and_return(true) + allow(controller.gpo_mail_service).to receive(:rate_limited?).and_return(true) allow(subject.idv_session).to receive(:address_mechanism_chosen?). and_return(true) get :index @@ -63,7 +63,7 @@ end it 'allows a user to request another letter' do - allow(controller.gpo_mail_service).to receive(:mail_spammed?).and_return(false) + allow(controller.gpo_mail_service).to receive(:rate_limited?).and_return(false) get :index expect(response).to be_ok diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index dcc4abcc8bf..4c2a80993d5 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -27,6 +27,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::DocumentCaptureController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( @@ -103,6 +109,8 @@ context 'hybrid handoff step is not complete' do it 'redirects to hybrid handoff' do + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = nil get :show diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 69252dda3ed..426e0ba23e9 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -15,6 +15,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::HybridHandoffController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( @@ -30,20 +36,6 @@ ) end - it 'checks that agreement step is complete' do - expect(subject).to have_actions( - :before, - :confirm_agreement_step_complete, - ) - end - - it 'checks that hybrid_handoff is needed' do - expect(subject).to have_actions( - :before, - :confirm_hybrid_handoff_needed, - ) - end - it 'includes redirect for phone_question ab test before_action' do expect(subject).to have_actions( :before, @@ -86,6 +78,7 @@ context 'agreement step is not complete' do before do + subject.idv_session.welcome_visited = true subject.idv_session.idv_consent_given = nil end @@ -97,7 +90,7 @@ end context 'hybrid_handoff already visited' do - it 'redirects to document_capture in standard flow' do + it 'shows hybrid_handoff' do subject.idv_session.flow_path = 'standard' get :show @@ -105,7 +98,7 @@ expect(response).to render_template :show end - it 'redirects to link_sent in hybrid flow' do + it 'shows hybrid_handoff' do subject.idv_session.flow_path = 'hybrid' get :show @@ -313,10 +306,9 @@ expect(response).to redirect_to(idv_phone_question_url) end - context 'when refered by phone_question page' do - let(:referer) { idv_phone_question_url } + context 'when user comes from phone_question page' do before do - request.env['HTTP_REFERER'] = referer + subject.idv_session.phone_with_camera = false end it 'does not redirect users away from hybrid handoff page' do diff --git a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb index 4e60db66461..e5b225ef78f 100644 --- a/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/document_capture_controller_spec.rb @@ -58,6 +58,7 @@ flow_path: 'hybrid', irs_reproofing: false, step: 'document_capture', + phone_with_camera: nil, }.merge(ab_test_args) end @@ -80,6 +81,18 @@ expect(@analytics).to have_logged_event(analytics_name, analytics_args) end + context 'user visited phone_question_page' do + before do + allow(AbTests::IDV_PHONE_QUESTION).to receive(:bucket).and_return(:show_phone_question) + end + it 'logs user has a phone_with_camera' do + get :show + + expect(@analytics). + to have_logged_event(analytics_name, analytics_args.merge(phone_with_camera: true)) + end + end + it 'updates DocAuthLog document_capture_view_count' do doc_auth_log = DocAuthLog.create(user_id: user.id) @@ -159,6 +172,7 @@ flow_path: 'hybrid', irs_reproofing: false, step: 'document_capture', + phone_with_camera: nil, }.merge(ab_test_args) end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index c19f70e1177..6a9ef3d38ca 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -416,6 +416,7 @@ back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, + classification_info: a_kind_of(Hash), ) expect(@irs_attempts_api_tracker).to receive(:track_event).with( @@ -468,6 +469,8 @@ let(:state) { 'ND' } let(:state_id_type) { 'drivers_license' } let(:dob) { '10/06/1938' } + let(:country_code) { 'USA' } + let(:class_name) { 'Identification Card' } before do DocAuth::Mock::DocAuthMockClient.mock_response!( @@ -475,7 +478,20 @@ response: DocAuth::Response.new( success: true, errors: {}, - extra: { doc_auth_result: 'Passed', billed: true }, + extra: { + doc_auth_result: 'Passed', + billed: true, + classification_info: { + Front: { + CountryCode: country_code, + ClassName: class_name, + }, + Back: { + CountryCode: country_code, + ClassName: class_name, + }, + }, + }, pii_from_doc: { first_name: first_name, last_name: last_name, @@ -586,6 +602,10 @@ back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, + classification_info: hash_including( + Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), + Back: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), + ), ) expect(@irs_attempts_api_tracker).to receive(:track_event).with( @@ -679,6 +699,10 @@ back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, + classification_info: hash_including( + Front: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), + Back: hash_including(ClassName: 'Identification Card', CountryCode: 'USA'), + ), ) expect(@irs_attempts_api_tracker).to receive(:track_event).with( @@ -772,6 +796,7 @@ back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, + classification_info: hash_including(:Front, :Back), ) expect(@irs_attempts_api_tracker).to receive(:track_event).with( diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb new file mode 100644 index 00000000000..17de564e6ef --- /dev/null +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -0,0 +1,118 @@ +require 'rails_helper' + +RSpec.describe Idv::InPerson::AddressController do + include InPersonHelper + let(:in_person_residential_address_controller_enabled) { true } + let(:pii_from_user) { Idp::Constants::MOCK_IPP_APPLICANT_SAME_ADDRESS_AS_ID_FALSE.dup } + let(:user) { build(:user) } + let(:flow_session) do + { pii_from_user: pii_from_user } + end + let(:flow_path) { 'standard' } + + before(:each) do + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(true) + allow(subject).to receive(:current_user). + and_return(user) + allow(subject).to receive(:pii_from_user).and_return(pii_from_user) + allow(subject).to receive(:flow_session).and_return(flow_session) + allow(subject).to receive(:flow_path).and_return(flow_path) + stub_sign_in(user) + stub_analytics + allow(@analytics).to receive(:track_event) + end + + describe 'before_actions' do + context '#render_404_if_in_person_residential_address_controller_enabled not set' do + context 'flag not set' do + before do + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(nil) + end + it 'renders a 404' do + get :show + + expect(response).to be_not_found + end + end + + context 'flag not enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(false) + end + it 'renders a 404' do + get :show + + expect(response).to be_not_found + end + end + end + + context '#confirm_in_person_state_id_step_complete' do + it 'redirects to state id page if not complete' do + flow_session[:pii_from_user].delete(:identity_doc_address1) + get :show + + expect(response).to redirect_to idv_in_person_step_url(step: :state_id) + end + end + + context '#confirm_in_person_address_step_needed' do + context 'step is not needed' do + it 'redirects to ssn page when same address as id is true' do + flow_session[:pii_from_user][:same_address_as_id] = 'true' + get :show + expect(response).to redirect_to idv_in_person_ssn_url + end + + it 'redirects to ssn page when address1 present' do + flow_session[:pii_from_user][:address1] = '123 Main St' + get :show + expect(response).to redirect_to idv_in_person_ssn_url + end + end + end + end + + describe '#show' do + let(:analytics_name) { 'IdV: in person proofing address visited' } + let(:analytics_args) do + { + analytics_id: 'In Person Proofing', + flow_path: flow_path, + irs_reproofing: false, + step: 'address', + step_count: nil, + } + end + + context 'with address controller flag enabled' do + it 'renders the show template' do + get :show + + expect(response).to render_template :show + end + + it 'logs idv_in_person_proofing_address_visited' do + get :show + + expect(@analytics).to have_received( + :track_event, + ).with(analytics_name, analytics_args) + end + + it 'has correct extra_view_variables' do + expect(subject.extra_view_variables).to include( + form: Idv::InPerson::AddressForm, + updating_address: false, + ) + + expect(subject.extra_view_variables[:pii]).to_not have_key( + :address1, + ) + end + end + end +end diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index 127eae851d3..beb74c7a78d 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -28,16 +28,40 @@ describe 'before_actions' do context('#confirm_in_person_address_step_complete') do - it 'redirects if the user hasn\'t completed the address page' do - # delete address attributes on session - flow_session[:pii_from_user].delete(:address1) - flow_session[:pii_from_user].delete(:address2) - flow_session[:pii_from_user].delete(:city) - flow_session[:pii_from_user].delete(:state) - flow_session[:pii_from_user].delete(:zipcode) - get :show - - expect(response).to redirect_to idv_in_person_step_url(step: :address) + context 'residential address controller flag not enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(false) + end + it 'redirects if the user hasn\'t completed the address page' do + # delete address attributes on session + flow_session[:pii_from_user].delete(:address1) + flow_session[:pii_from_user].delete(:address2) + flow_session[:pii_from_user].delete(:city) + flow_session[:pii_from_user].delete(:state) + flow_session[:pii_from_user].delete(:zipcode) + get :show + + expect(response).to redirect_to idv_in_person_step_url(step: :address) + end + end + + context 'residential address controller flag enabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(true) + end + it 'redirects if address page not completed' do + # delete address attributes on session + flow_session[:pii_from_user].delete(:address1) + flow_session[:pii_from_user].delete(:address2) + flow_session[:pii_from_user].delete(:city) + flow_session[:pii_from_user].delete(:state) + flow_session[:pii_from_user].delete(:zipcode) + get :show + + expect(response).to redirect_to idv_in_person_proofing_address_url + end end end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index c1a1394d4f2..d6904a2e477 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -16,6 +16,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::LinkSentController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( @@ -73,6 +79,8 @@ context '#confirm_hybrid_handoff_complete' do context 'no flow_path' do it 'redirects to idv_hybrid_handoff_url' do + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = nil get :show @@ -83,6 +91,8 @@ context 'flow_path is standard' do it 'redirects to idv_document_capture_url' do + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = 'standard' get :show diff --git a/spec/controllers/idv/phone_question_controller_spec.rb b/spec/controllers/idv/phone_question_controller_spec.rb index d97e9917699..9d56bf640a7 100644 --- a/spec/controllers/idv/phone_question_controller_spec.rb +++ b/spec/controllers/idv/phone_question_controller_spec.rb @@ -23,6 +23,13 @@ subject.user_session['idv/doc_auth'] = {} subject.idv_session.idv_consent_given = true allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) + allow(AbTests::IDV_PHONE_QUESTION).to receive(:bucket).and_return(:show_phone_question) + end + + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::PhoneQuestionController.step_info).to be_valid + end end describe 'before_actions' do @@ -40,13 +47,6 @@ ) end - it 'checks that agreement step is complete' do - expect(subject).to have_actions( - :before, - :confirm_agreement_step_complete, - ) - end - it 'checks that hybrid_handoff is needed' do expect(subject).to have_actions( :before, @@ -82,6 +82,7 @@ context 'agreement step is not complete' do before do + subject.idv_session.welcome_visited = true subject.idv_session.idv_consent_given = nil end @@ -90,6 +91,12 @@ expect(response).to redirect_to(idv_agreement_url) end + + it 'phone_with_camera not set in idv_session' do + get :phone_with_camera + + expect(subject.idv_session.phone_with_camera).to be_nil + end end context 'confirm_hybrid_handoff_needed before action' do @@ -147,7 +154,12 @@ get :phone_with_camera expect(@analytics). - to have_logged_event(analytics_name, analytics_args.merge!(phone_with_camera: true)) + to have_logged_event(analytics_name, analytics_args) + end + + it 'phone_with_camera set in idv_session' do + expect { get :phone_with_camera }. + to change { subject.idv_session.phone_with_camera }.from(nil).to true end end @@ -164,12 +176,17 @@ get :phone_without_camera expect(@analytics). - to have_logged_event(analytics_name, analytics_args.merge!(phone_with_camera: false)) + to have_logged_event(analytics_name, analytics_args) end it 'set idv_session flow path to standard' do expect { get :phone_without_camera }. to change { subject.idv_session.flow_path }.from(nil).to 'standard' end + + it 'phone_with_camera set in idv_session' do + expect { get :phone_without_camera }. + to change { subject.idv_session.phone_with_camera }.from(nil).to false + end end end diff --git a/spec/controllers/idv/welcome_controller_spec.rb b/spec/controllers/idv/welcome_controller_spec.rb index a9db32766b6..ed2873990a4 100644 --- a/spec/controllers/idv/welcome_controller_spec.rb +++ b/spec/controllers/idv/welcome_controller_spec.rb @@ -13,6 +13,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::WelcomeController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( diff --git a/spec/factories/service_providers.rb b/spec/factories/service_providers.rb index bc77f9885bc..bfaf9449d4f 100644 --- a/spec/factories/service_providers.rb +++ b/spec/factories/service_providers.rb @@ -7,6 +7,7 @@ issuer { SecureRandom.uuid } return_to_sp_url { '/' } agency { association :agency } + launch_date { Date.new(2020, 1, 1) } help_text do { sign_in: { en: 'custom sign in help text for %{sp_name}' }, sign_up: { en: 'custom sign up help text for %{sp_name}' }, @@ -33,6 +34,14 @@ end end + trait :idv do + ial { 2 } + end + + trait :active do + active { true } + end + trait :irs do friendly_name { 'An IRS Service Provider' } ial { 2 } diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 8d81db55777..c8faaf3f57a 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -39,28 +39,28 @@ { 'IdV: intro visited' => {}, 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: consent checkbox toggled' => { checked: true, }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' @@ -72,33 +72,33 @@ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question }, 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {} }, 'IdV: doc auth document_capture submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth ssn visited' => { - flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify visited' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { - success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { @@ -117,15 +117,15 @@ proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: review info visited' => { - address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: review complete' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { @@ -147,25 +147,25 @@ { 'IdV: intro visited' => {}, 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' @@ -177,52 +177,52 @@ success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question }, 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {} }, 'IdV: doc auth document_capture submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn visited' => { - flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify visited' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: USPS address letter requested' => { - resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: request letter visited' => { letter_already_sent: false, }, 'IdV: review info visited' => { - address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: USPS address letter enqueued' => { - enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, + enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: review complete' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: letter enqueued visited' => { @@ -234,25 +234,25 @@ let(:in_person_path_events) do { 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' @@ -292,23 +292,23 @@ success: true, step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: false }, 'IdV: doc auth ssn visited' => { - analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false + analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false }, 'IdV: doc auth ssn submitted' => { - analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false + analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false }, 'IdV: doc auth verify visited' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, 'IdV: doc auth verify submitted' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: 'aaa-bbb-ccc', success: true, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone confirmation form' => { - success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation vendor' => { @@ -327,15 +327,15 @@ proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: review info visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, address_verification_method: 'phone', + acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, address_verification_method: 'phone', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: review complete' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { diff --git a/spec/features/idv/clearing_and_restarting_spec.rb b/spec/features/idv/clearing_and_restarting_spec.rb index f1fbc6408ba..8e752b26ee4 100644 --- a/spec/features/idv/clearing_and_restarting_spec.rb +++ b/spec/features/idv/clearing_and_restarting_spec.rb @@ -5,7 +5,7 @@ let(:user) { user_with_2fa } - context 'during GPO otp verification', js: true do + context 'during verification code entry', js: true do before do start_idv_from_sp complete_idv_steps_with_gpo_before_confirmation_step(user) diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index cad9cfa9349..eedb3fef6bc 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -134,20 +134,6 @@ expect(DocAuthLog.find_by(user_id: user.id).state).to be_nil end - - it 'return to sp when click on exit link', :js do - click_sp_exit_link(sp_name: sp_name) - expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') - end - - it 'logs event and return to sp when click on submit and exit button', :js do - click_submit_exit_button - expect(fake_analytics).to have_logged_event( - 'Frontend: IdV: exit optional questions', - hash_including('ids'), - ) - expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') - end end context 'standard mobile flow' do @@ -176,16 +162,6 @@ expect(page).to have_current_path(idv_phone_url) end end - - it 'return to sp when click on exit link', :js do - perform_in_browser(:mobile) do - visit_idp_from_oidc_sp_with_ial2 - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_document_capture_step - click_sp_exit_link(sp_name: sp_name) - expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') - end - end end def expect_costing_for_document diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index b6bd723911b..0fadeb473f4 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -180,8 +180,7 @@ context 'PhoneQuestion page' do before do - allow_any_instance_of(Idv::HybridHandoffController). - to receive(:phone_question_ab_test_bucket).and_return(:show_phone_question) + allow(AbTests::IDV_PHONE_QUESTION).to receive(:bucket).and_return(:show_phone_question) end it 'rate limits sending the link' do diff --git a/spec/features/idv/doc_auth/phone_question_spec.rb b/spec/features/idv/doc_auth/phone_question_spec.rb index d5ad26cc1bd..370118a78c3 100644 --- a/spec/features/idv/doc_auth/phone_question_spec.rb +++ b/spec/features/idv/doc_auth/phone_question_spec.rb @@ -16,9 +16,9 @@ before do allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + allow(AbTests::IDV_PHONE_QUESTION).to receive(:bucket).and_return(:show_phone_question) sign_in_and_2fa_user complete_doc_auth_steps_before_hybrid_handoff_step - visit(idv_phone_question_url) end context 'phone question answered' do @@ -28,6 +28,7 @@ let(:phone_number) { '415-555-0199' } it 'redirects to hybrid handoff if user confirms having phone' do + expect(page).to have_current_path(idv_phone_question_path) click_link t('doc_auth.buttons.have_phone') expect(page).to have_current_path(idv_hybrid_handoff_path) expect(fake_analytics).to have_logged_event( @@ -40,6 +41,12 @@ expect(page).to have_current_path(idv_link_sent_path) click_link(t('forms.buttons.back')) expect(page).to have_current_path(idv_hybrid_handoff_path(redo: true)) + + # test no, keep going from cancel page + click_link t('links.cancel') + expect(current_path).to eq(idv_cancel_path) + click_on t('idv.cancel.actions.keep_going') + expect(page).to have_current_path(idv_hybrid_handoff_path(redo: true)) end end diff --git a/spec/features/idv/steps/enter_code_step_spec.rb b/spec/features/idv/steps/enter_code_step_spec.rb index 32bd399f366..03bebd39bd8 100644 --- a/spec/features/idv/steps/enter_code_step_spec.rb +++ b/spec/features/idv/steps/enter_code_step_spec.rb @@ -29,7 +29,7 @@ and_return(threatmetrix_enabled ? :enabled : :disabled) end - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' context 'ThreatMetrix disabled, but we have ThreatMetrix status on proofing component' do let(:threatmetrix_enabled) { false } @@ -41,14 +41,14 @@ fraud_pending_reason: 'threatmetrix_review', ) end - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' end context 'ThreatMetrix enabled' do let(:threatmetrix_enabled) { true } context 'ThreatMetrix says "pass"' do - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' end context 'ThreatMetrix says "review"' do @@ -61,7 +61,7 @@ fraud_pending_reason: 'threatmetrix_review', ) end - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' end context 'ThreatMetrix says "reject"' do @@ -74,18 +74,18 @@ fraud_pending_reason: 'threatmetrix_reject', ) end - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' end context 'No ThreatMetrix result on proofing component' do - it_behaves_like 'gpo otp verification' + it_behaves_like 'verification code entry' end end context 'coming from an "I did not receive my letter" link in a reminder email' do it 'renders an alternate ui', :js do visit idv_verify_by_mail_enter_code_url(did_not_receive_letter: 1) - verify_no_spam_warning_banner + verify_no_rate_limit_banner expect(current_path).to eql(new_user_session_path) fill_in_credentials_and_submit(user.email, user.password) @@ -108,7 +108,7 @@ sign_in_live_with_2fa(user) expect(current_path).to eq idv_verify_by_mail_enter_code_path - verify_no_spam_warning_banner + verify_no_rate_limit_banner expect(page).to have_content t('idv.messages.gpo.resend') fill_in t('idv.gpo.form.otp_label'), with: otp @@ -138,7 +138,7 @@ expect(current_path).to eq idv_verify_by_mail_enter_code_path expect(page).to have_content t('idv.messages.gpo.resend') - verify_no_spam_warning_banner + verify_no_rate_limit_banner gpo_confirmation_code fill_in t('idv.gpo.form.otp_label'), with: otp click_button t('idv.gpo.form.submit') @@ -156,7 +156,7 @@ expect(page).to have_content strip_tags(t('idv.gpo.change_to_verification_code_html')) expect(page).to have_content t('idv.gpo.wrong_address') expect(page).to have_content Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:address1] - verify_no_spam_warning_banner + verify_no_rate_limit_banner click_on t('idv.gpo.clear_and_start_over') @@ -178,7 +178,7 @@ sign_in_live_with_2fa(user) expect(current_path).to eq idv_verify_by_mail_enter_code_path - verify_spam_warning_banner_present(another_gpo_confirmation_code.updated_at) + verify_rate_limit_banner_present(another_gpo_confirmation_code.updated_at) click_on t('idv.messages.clear_and_start_over') @@ -222,15 +222,15 @@ end it 'shows a warning message and does not allow the user to request another letter' do - verify_spam_warning_banner_present(code_sent_at) + verify_rate_limit_banner_present(code_sent_at) expect(page).not_to have_content t('idv.messages.gpo.resend') end end - def verify_no_spam_warning_banner + def verify_no_rate_limit_banner expect(page).not_to have_content( t( - 'idv.gpo.alert_spam_warning_html', + 'idv.gpo.alert_rate_limit_warning_html', date_letter_was_sent: I18n.l( Time.zone.now, format: :event_date, @@ -239,10 +239,10 @@ def verify_no_spam_warning_banner ) end - def verify_spam_warning_banner_present(code_sent_at = Time.zone.now) + def verify_rate_limit_banner_present(code_sent_at = Time.zone.now) expect(page).to have_content strip_tags( t( - 'idv.gpo.alert_spam_warning_html', + 'idv.gpo.alert_rate_limit_warning_html', date_letter_was_sent: I18n.l( code_sent_at, format: :event_date, diff --git a/spec/features/idv/steps/in_person/address_spec.rb b/spec/features/idv/steps/in_person/address_spec.rb new file mode 100644 index 00000000000..be69a8687b0 --- /dev/null +++ b/spec/features/idv/steps/in_person/address_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +RSpec.describe 'doc auth In person proofing residential address step', js: true do + include IdvStepHelper + include InPersonHelper + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_residential_address_controller_enabled). + and_return(true) + end + + context 'when visiting address for the first time' do + it 'displays correct heading and button text', allow_browser_log: true do + complete_idv_steps_before_address + # residential address page + expect(page).to have_current_path(idv_in_person_proofing_address_url) + + expect(page).to have_content(t('forms.buttons.continue')) + expect(page).to have_content(t('in_person_proofing.headings.address')) + end + + it 'allows the user to cancel and start over', allow_browser_log: true do + complete_idv_steps_before_address + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.start_over') + expect(page).to have_current_path(idv_welcome_path) + end + + it 'allows the user to cancel and return', allow_browser_log: true do + complete_idv_steps_before_address + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.keep_going') + expect(page).to have_current_path(idv_in_person_proofing_address_url) + end + end +end diff --git a/spec/fixtures/ial2_test_credential_classification_info_no_name.yml b/spec/fixtures/ial2_test_credential_classification_info_no_name.yml new file mode 100644 index 00000000000..1452f12ee24 --- /dev/null +++ b/spec/fixtures/ial2_test_credential_classification_info_no_name.yml @@ -0,0 +1,18 @@ +document: + address1: 1800 F Street + address2: Apt 3 + city: Bayside + state: NY + zipcode: '11364' + dob: 10/06/1938 + phone: +1 314-555-1212 + state_id_jurisdiction: 'ND' +doc_auth_result: Passed +failed_alert: [] +classification_info: + Front: + ClassName: Drivers License + CountryCode: USA + Back: + ClassName: Drivers License + CountryCode: USA diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index eebb205040f..babc834a833 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -14,20 +14,21 @@ class BaseTask # List of keys allowed to be untranslated or are the same as English ALLOWED_UNTRANSLATED_KEYS = [ { key: 'account.navigation.menu', locales: %i[fr] }, # "Menu" is "Menu" in French + { key: /^countries/ }, # Some countries have the same name across languages + { 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: 'doc_auth.headings.photo', locales: %i[fr] }, # "Photo" is "Photo" in French { key: /^i18n\.locale\./ }, # Show locale options translated as that language { key: /^i18n\.transliterate\./ }, # Approximate non-ASCII characters in ASCII - { key: /^countries/ }, # Some countries have the same name across languages { key: 'links.contact', locales: %i[fr] }, # "Contact" is "Contact" in French + { key: 'mailer.logo' }, # "logo is logo" in English, French and Spanish + { key: 'saml_idp.auth.error.title', locales: %i[es] }, # "Error" is "Error" in Spanish { key: 'simple_form.no', locales: %i[es] }, # "No" is "No" in Spanish { key: 'simple_form.required.html' }, # No text content { key: 'simple_form.required.mark' }, # No text content { key: 'time.am' }, # "AM" is "AM" in French and Spanish - { key: 'time.pm' }, # "PM" is "PM" in French and Spanish { key: 'time.formats.sms_date' }, # for us date format - { 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: 'mailer.logo' }, # "logo is logo" in English, French and Spanish + { key: 'time.pm' }, # "PM" is "PM" in French and Spanish ].freeze def untranslated_keys diff --git a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx index d640c75a01c..56de2aaa029 100644 --- a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx +++ b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx @@ -68,7 +68,6 @@ describe('DocumentCaptureReviewIssues', () => { expect(backCapture).to.be.ok(); expect(getByText('back side error')).to.be.ok(); expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); - expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); it('renders for a doc type failure', () => { @@ -115,7 +114,6 @@ describe('DocumentCaptureReviewIssues', () => { expect(backCapture).to.be.ok(); expect(getByText('back side doc type error')).to.be.ok(); expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); - expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); }); }); diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx index ecff722a4d4..d160d95c5b4 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx @@ -82,16 +82,4 @@ describe('document-capture/components/documents-step', () => { expect(queryByText(notExpectedText)).to.not.exist(); }); - - it('renders optional question part', () => { - const { getByRole, getByText } = render( - - - - - , - ); - expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); - expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); - }); }); diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx index d24b71cfd1a..fac01242337 100644 --- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx @@ -343,14 +343,6 @@ describe('document-capture/components/review-issues-step', () => { expect(getByLabelText('doc_auth.headings.document_capture_back')).to.be.ok(); }); - it('renders optional questions', async () => { - const { getByText, getByRole } = render(); - - await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' })); - expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); - expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); - }); - context('service provider context', () => { context('ial2', () => { it('renders with front and back inputs', async () => { diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb index 12faf041904..38647a3e138 100644 --- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb +++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb @@ -9,22 +9,20 @@ let(:report_folder) do 'int/monthly-key-metrics-report/2021/2021-03-02.monthly-key-metrics-report' end - let(:account_reuse_s3_path) { "#{report_folder}/account_reuse.csv" } - let(:total_profiles_s3_path) { "#{report_folder}/total_profiles.csv" } - let(:document_upload_proofing_s3_path) { "#{report_folder}/document_upload_proofing.csv" } - let(:account_deletion_rate_s3_path) { "#{report_folder}/account_deletion_rate.csv" } - let(:total_user_count_s3_path) { "#{report_folder}/total_user_count.csv" } - let(:active_users_count_s3_path) { "#{report_folder}/active_users_count.csv" } + let(:expected_s3_paths) do [ - account_reuse_s3_path, - total_profiles_s3_path, - account_deletion_rate_s3_path, - total_user_count_s3_path, - document_upload_proofing_s3_path, - active_users_count_s3_path, + "#{report_folder}/account_reuse.csv", + "#{report_folder}/total_profiles.csv", + "#{report_folder}/document_upload_proofing.csv", + "#{report_folder}/account_deletion_rate.csv", + "#{report_folder}/total_user_count.csv", + "#{report_folder}/active_users_count.csv", + "#{report_folder}/proofing_rate_metrics.csv", + "#{report_folder}/agency_and_sp_counts.csv", ] end + let(:s3_metadata) do { body: anything, @@ -39,6 +37,12 @@ ] end + let(:mock_proofing_rate_data) do + [ + ['Metric', 'Trailing 30d', 'Trailing 60d', 'Trailing 90d'], + ] + end + before do allow(Identity::Hostdata).to receive(:env).and_return('int') allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') @@ -54,6 +58,8 @@ allow(report.monthly_proofing_report).to receive(:proofing_report). and_return(mock_proofing_report_data) + allow(report.proofing_rate_report).to receive(:as_csv). + and_return(mock_proofing_rate_data) end it 'sends out a report to the email listed with one total user' do diff --git a/spec/lib/base16_spec.rb b/spec/lib/base16_spec.rb deleted file mode 100644 index 69130cb4e37..00000000000 --- a/spec/lib/base16_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' -require 'base16' - -RSpec.describe Base16 do - context 'with reasonable inputs' do - context 'given "Hello, World"' do - let(:input) { 'Hello, world' } - - it 'returns a known value' do - encoded = described_class.encode16(input) - # The IRS confirmed this value: - expect(encoded).to eq '48656C6C6F2C20776F726C64' - - decoded = described_class.decode16(encoded) - expect(decoded).to eq input - end - - it 'returns a value with uppercase letters' do - encoded = described_class.encode16(input) - expect(encoded).to eq(encoded.upcase) - end - end - - context 'given a sequence of zeroes' do - let(:input) { "\x00" } - it 'does not truncate them' do - encoded = described_class.encode16(input) - expect(encoded).to eq '00' - - decoded = described_class.decode16(encoded) - expect(decoded).to eq(input) - end - end - end - - context 'with a less reasonable input' do - context 'given a zany-face emoji' do - let(:input) { '🤪' } - it 'returns the same bytes' do - encoded = described_class.encode16(input) - decoded = described_class.decode16(encoded).force_encoding('UTF-8') - expect(decoded).to eq input - end - end - end -end diff --git a/spec/lib/reporting/cloudwatch_client_spec.rb b/spec/lib/reporting/cloudwatch_client_spec.rb index 2fcd16f3514..6a6f1c48e49 100644 --- a/spec/lib/reporting/cloudwatch_client_spec.rb +++ b/spec/lib/reporting/cloudwatch_client_spec.rb @@ -244,5 +244,28 @@ def stub_single_page expect(logger_io.string).to include('is before Cloudwatch Insights availability') end end + + context 'when the query is outside the log retention range' do + before do + # rubocop:disable Layout/LineLength + Aws.config[:cloudwatchlogs] = { + stub_responses: { + start_query: Aws::CloudWatchLogs::Errors::MalformedQueryException.new( + nil, + 'end date and time is either before the log groups creation time or exceeds the log groups log retention settings', + ), + }, + } + # rubocop:enable Layout/LineLength + + allow(Time).to receive(:zone).and_return(nil) + end + + it 'logs a warning and returns an empty array for that range' do + expect(fetch).to eq([]) + + expect(logger_io.string).to include('exceeds the log groups log retention settings') + end + end end end diff --git a/spec/lib/reporting/monthly_proofing_report_spec.rb b/spec/lib/reporting/monthly_proofing_report_spec.rb index db58bcf31b7..5e00fee39af 100644 --- a/spec/lib/reporting/monthly_proofing_report_spec.rb +++ b/spec/lib/reporting/monthly_proofing_report_spec.rb @@ -58,28 +58,6 @@ end end - describe '#proofing_report' do - context 'when the data is outside the log retention range' do - before do - allow(report.cloudwatch_client).to receive(:fetch).and_raise( - Aws::CloudWatchLogs::Errors::MalformedQueryException.new( - nil, - 'exceeds the log groups log retention settings', - ), - ) - end - - it 'handles the error and returns a table with information on the error' do - expect(report.proofing_report).to match( - [ - ['Error', 'Message'], - ['Aws::CloudWatchLogs::Errors::MalformedQueryException', kind_of(String)], - ], - ) - end - end - end - describe '#data' do it 'keeps unique users per event as a hash' do expect(report.data).to eq( diff --git a/spec/lib/reporting/proofing_rate_report_spec.rb b/spec/lib/reporting/proofing_rate_report_spec.rb index 66bbdfe088c..508e7e2fafa 100644 --- a/spec/lib/reporting/proofing_rate_report_spec.rb +++ b/spec/lib/reporting/proofing_rate_report_spec.rb @@ -100,22 +100,19 @@ expect(Reporting::IdentityVerificationReport).to have_received(:new).with( time_range: (end_date - 30.days)..end_date, issuers: nil, - verbose: false, - progress: false, + cloudwatch_client: report.cloudwatch_client, ).once expect(Reporting::IdentityVerificationReport).to have_received(:new).with( time_range: (end_date - 60.days)..(end_date - 30.days), issuers: nil, - verbose: false, - progress: false, + cloudwatch_client: report.cloudwatch_client, ).once expect(Reporting::IdentityVerificationReport).to have_received(:new).with( time_range: (end_date - 90.days)..(end_date - 60.days), issuers: nil, - verbose: false, - progress: false, + cloudwatch_client: report.cloudwatch_client, ).once end end diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb index 4c247c4c57b..248d2508591 100644 --- a/spec/mailers/previews/report_mailer_preview.rb +++ b/spec/mailers/previews/report_mailer_preview.rb @@ -11,6 +11,7 @@ def warn_error def monthly_key_metrics_report monthly_key_metrics_report = Reports::MonthlyKeyMetricsReport.new(Time.zone.today) + stub_cloudwatch_client(monthly_key_metrics_report.proofing_rate_report) stub_cloudwatch_client(monthly_key_metrics_report.monthly_proofing_report) ReportMailer.tables_report( diff --git a/spec/policies/idv/flow_policy_spec.rb b/spec/policies/idv/flow_policy_spec.rb new file mode 100644 index 00000000000..e26c79cd8b1 --- /dev/null +++ b/spec/policies/idv/flow_policy_spec.rb @@ -0,0 +1,80 @@ +require 'rails_helper' + +RSpec.describe 'Idv::FlowPolicy' do + include Rails.application.routes.url_helpers + + let(:user) { create(:user) } + + let(:idv_session) do + Idv::Session.new( + user_session: {}, + current_user: user, + service_provider: nil, + ) + end + + let(:user_phone_confirmation_session) { nil } + let(:has_gpo_pending_profile) { nil } + + subject { Idv::FlowPolicy.new(idv_session: idv_session, user: user) } + + context '#controller_allowed?' do + it 'allows the welcome step' do + expect(subject.controller_allowed?(controller: Idv::WelcomeController)).to be true + end + end + + context 'each step in the flow' do + before do + allow(Idv::PhoneConfirmationSession).to receive(:from_h). + with(user_phone_confirmation_session).and_return(user_phone_confirmation_session) + allow(user).to receive(:gpo_pending_profile?).and_return(has_gpo_pending_profile) + end + context 'empty session' do + it 'returns welcome' do + expect(subject.info_for_latest_step.key).to eq(:welcome) + end + end + + context 'preconditions for agreement are present' do + it 'returns agreement' do + idv_session.welcome_visited = true + expect(subject.info_for_latest_step.key).to eq(:agreement) + expect(subject.controller_allowed?(controller: Idv::AgreementController)).to be + expect(subject.controller_allowed?(controller: Idv::HybridHandoffController)).not_to be + end + end + + context 'preconditions for hybrid_handoff are present' do + it 'returns hybrid_handoff' do + idv_session.welcome_visited = true + idv_session.idv_consent_given = true + expect(subject.info_for_latest_step.key).to eq(:hybrid_handoff) + expect(subject.controller_allowed?(controller: Idv::HybridHandoffController)).to be + expect(subject.controller_allowed?(controller: Idv::DocumentCaptureController)).not_to be + end + end + + context 'preconditions for document_capture are present' do + it 'returns document_capture' do + idv_session.welcome_visited = true + idv_session.idv_consent_given = true + idv_session.flow_path = 'standard' + expect(subject.info_for_latest_step.key).to eq(:document_capture) + expect(subject.controller_allowed?(controller: Idv::DocumentCaptureController)).to be + # expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be + end + end + + context 'preconditions for link_sent are present' do + it 'returns link_sent' do + idv_session.welcome_visited = true + idv_session.idv_consent_given = true + idv_session.flow_path = 'hybrid' + expect(subject.info_for_latest_step.key).to eq(:link_sent) + expect(subject.controller_allowed?(controller: Idv::LinkSentController)).to be + # expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be + end + end + end +end diff --git a/spec/policies/idv/step_info_spec.rb b/spec/policies/idv/step_info_spec.rb new file mode 100644 index 00000000000..8a3da3faa60 --- /dev/null +++ b/spec/policies/idv/step_info_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Idv::StepInfo' do + let(:controller) { ApplicationController.controller_name } + let(:next_steps) { [] } + let(:preconditions) { ->(idv_session:, user:) { true } } + subject do + Idv::StepInfo.new( + key: :my_key, + controller: controller, + next_steps: next_steps, + preconditions: preconditions, + ) + end + + context 'when given valid arguments' do + it 'succeeds' do + expect(subject).to be_valid + end + end + + context 'when given an invalid next_steps' do + let(:next_steps) { 'foo' } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when given an invalid preconditions' do + let(:preconditions) { 'foo' } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb index 6779d2d8235..db8afa1c1f6 100644 --- a/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/lexis_nexis_client_spec.rb @@ -26,38 +26,6 @@ end end - describe '#post_front_image' do - it 'raises a NotImplemented error' do - expect do - client.post_front_image( - instance_id: 123, - image: DocAuthImageFixtures.document_front_image, - ) - end.to raise_error(NotImplementedError) - end - end - - describe '#post_back_image' do - it 'raises a NotImplemented error' do - expect do - client.post_back_image( - instance_id: 123, - image: DocAuthImageFixtures.document_back_image, - ) - end.to raise_error(NotImplementedError) - end - end - - describe '#get_results' do - it 'raises a NotImplemented error' do - expect do - client.get_results( - instance_id: 123, - ) - end.to raise_error(NotImplementedError) - end - end - describe '#post_images' do before do stub_request(:post, image_upload_url).to_return( diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 41eb2fe912d..caa2df20f6f 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -64,6 +64,7 @@ it 'has extra attributes' do extra_attributes = response.extra_attributes expect(extra_attributes).not_to be_empty + expect(extra_attributes[:classification_info]).to include(:Front, :Back) end it 'has PII data' do # This is the minimum expected by doc_pii_form in the core IDP diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb index 41b40f1fc8c..d051a630073 100644 --- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb +++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb @@ -132,6 +132,7 @@ expect(errors[:front]).to contain_exactly(DocAuth::Errors::CARD_TYPE) expect(errors[:back]).to contain_exactly(DocAuth::Errors::CARD_TYPE) expect(errors[:hints]).to eq(true) + expect(get_results_response.extra[:classification_info]).to include(:Front, :Back) end it 'allows responses to be mocked' do described_class.mock_response!(method: :create_document, response: 'Create doc test') diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index f940ecd58aa..ce74c806ef5 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -301,6 +301,7 @@ expect(response.extra).to eq( doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name, billed: true, + classification_info: {}, ) end end @@ -327,6 +328,7 @@ expect(response.extra).to eq( doc_auth_result: DocAuth::Acuant::ResultCodes::CAUTION.name, billed: true, + classification_info: {}, ) end end @@ -351,6 +353,7 @@ expect(response.extra).to eq( doc_auth_result: DocAuth::Acuant::ResultCodes::FAILED.name, billed: true, + classification_info: {}, ) end end @@ -396,6 +399,7 @@ expect(response.extra).to eq( doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name, billed: true, + classification_info: {}, ) end end @@ -478,4 +482,122 @@ expect(response.doc_type_supported?).to eq(false) end end + context 'with a yaml file with a supported classname and country' do + let(:input) do + <<~YAML + doc_auth_result: Failed + classification_info: + Front: + ClassName: Drivers License + CountryCode: US + Back: + ClassName: Drivers License + CountryCode: US + YAML + end + it 'returns doc type as supported' do + expect(response.doc_type_supported?).to eq(true) + end + end + context 'with a yaml file with a supported classname and not supported country' do + let(:input) do + <<~YAML + doc_auth_result: Failed + classification_info: + Front: + ClassName: Drivers License + CountryCode: UK + Back: + ClassName: Drivers License + CountryCode: UK + YAML + end + it 'returns doc type as not supported' do + expect(response.doc_type_supported?).to eq(false) + end + end + context 'with a yaml file that does not include classification info' do + let(:input) do + <<~YAML + document: + first_name: Jane + last_name: Doe + middle_name: Q + city: Bayside + state: NY + zipcode: '11364' + dob: 10/06/1938 + phone: +1 314-555-1212 + state_id_jurisdiction: 'ND' + YAML + end + it 'successfully extracts PII' do + expect(response.pii_from_doc.empty?).to eq(false) + end + end + context 'with a yaml file that includes classification info' do + let(:input) do + <<~YAML + document: + first_name: Jane + last_name: Doe + middle_name: Q + city: Bayside + state: NY + zipcode: '11364' + dob: 10/06/1938 + phone: +1 314-555-1212 + state_id_jurisdiction: 'ND' + classification_info: + Front: + ClassName: Drivers License + CountryCode: USA + Back: + ClassName: Drivers License + CountryCode: USA + YAML + end + it 'successfully extracts classification info' do + classification_info = response.extra[:classification_info].deep_symbolize_keys + expect(classification_info).to eq( + { + Front: { ClassName: 'Drivers License', + CountryCode: 'USA' }, + Back: { ClassName: 'Drivers License', CountryCode: 'USA' }, + }, + ) + end + end + context 'with a yaml file that includes classification info but missing pii' do + let(:input) do + <<~YAML + doc_auth_result: Passed + document: + city: Bayside + state: NY + zipcode: '11364' + dob: 10/06/1938 + phone: +1 314-555-1212 + state_id_jurisdiction: 'ND' + failed_alerts: [] + classification_info: + Front: + ClassName: Drivers License + CountryCode: USA + Back: + ClassName: Drivers License + CountryCode: USA + YAML + end + it 'successfully extracts classification info' do + classification_info = response.extra[:classification_info].deep_symbolize_keys + expect(classification_info).to eq( + { + Front: { ClassName: 'Drivers License', + CountryCode: 'USA' }, + Back: { ClassName: 'Drivers License', CountryCode: 'USA' }, + }, + ) + end + end end diff --git a/spec/services/idv/gpo_mail_spec.rb b/spec/services/idv/gpo_mail_spec.rb index 953f1fae559..56d06e754e6 100644 --- a/spec/services/idv/gpo_mail_spec.rb +++ b/spec/services/idv/gpo_mail_spec.rb @@ -16,10 +16,10 @@ and_return(minimum_wait_before_another_usps_letter_in_hours) end - describe '#mail_spammed?' do + describe '#rate_limited?' do context 'when no letters have been requested' do it 'returns false' do - expect(subject.mail_spammed?).to eq false + expect(subject.rate_limited?).to eq false end end @@ -31,14 +31,14 @@ end it 'is true' do - expect(subject.mail_spammed?).to eq true + expect(subject.rate_limited?).to eq true end context 'but the window limit is disabled due to a 0 window size' do let(:letter_request_events_window_days) { 0 } it 'is false' do - expect(subject.mail_spammed?).to eq false + expect(subject.rate_limited?).to eq false end end @@ -46,7 +46,7 @@ let(:max_letter_request_events) { 0 } it 'is false' do - expect(subject.mail_spammed?).to eq false + expect(subject.rate_limited?).to eq false end end end @@ -57,14 +57,14 @@ end it 'is true' do - expect(subject.mail_spammed?).to eq true + expect(subject.rate_limited?).to eq true end context 'but the too-recent limit is disabled' do let(:minimum_wait_before_another_usps_letter_in_hours) { 0 } it 'is false' do - expect(subject.mail_spammed?).to eq false + expect(subject.rate_limited?).to eq false end end @@ -79,7 +79,7 @@ end it 'returns false' do - expect(subject.mail_spammed?).to be false + expect(subject.rate_limited?).to be false end end end diff --git a/spec/services/reporting/agency_and_sp_report_spec.rb b/spec/services/reporting/agency_and_sp_report_spec.rb new file mode 100644 index 00000000000..180adac48f4 --- /dev/null +++ b/spec/services/reporting/agency_and_sp_report_spec.rb @@ -0,0 +1,111 @@ +require 'csv' +require 'rails_helper' + +RSpec.describe Reporting::AgencyAndSpReport do + let(:report_date) do + Date.new(2021, 1, 1).in_time_zone('UTC') + end + + let(:header_row) do + ['', 'Number of apps (SPs)', 'Number of agencies'] + end + + before { travel_to report_date } + + # Wipe the pre-seeded data. It's easier to start from a clean slate. + before do + Agreements::IntegrationUsage.destroy_all + Agreements::IaaOrder.destroy_all + Agreements::Integration.destroy_all + Agreements::IntegrationStatus.destroy_all + Agreements::IaaGtc.destroy_all + Agreements::PartnerAccount.destroy_all + Agreements::PartnerAccountStatus.destroy_all + Agency.destroy_all + ServiceProvider.destroy_all + end + + subject(:report) { described_class.new(report_date) } + + describe '#agency_and_sp_report' do + subject { report.agency_and_sp_report } + + context 'when adding a non-IDV SP' do + let!(:auth_sp) { create(:service_provider, :active) } + let(:expected_report) do + [ + header_row, + ['Auth', 1, 1], + ['IDV', 0, 0], + ] + end + + it 'counts the SP and its Agency as auth (non-IDV)' do + expect(subject).to match_array(expected_report) + end + end + + context 'when adding an inactive SP' do + let!(:inactive_sp) { create(:service_provider) } + let(:expected_report) do + [ + header_row, + ['Auth', 0, 1], + ['IDV', 0, 0], + ] + end + + # Agencies don't have a sense of 'active' and are included. + it 'includes the agency but not the inactive SP' do + expect(subject).to match_array(expected_report) + end + end + + context 'when adding an IDV SP to a non-IDV Agency' do + let!(:initial_sp) { create(:service_provider, :active) } + let!(:agency) { initial_sp.agency } + + let(:initial_report) do + [ + header_row, + ['Auth', 1, 1], + ['IDV', 0, 0], + ] + end + + let(:updated_report) do + [ + header_row, + ['Auth', 1, 0], + ['IDV', 1, 1], + ] + end + + it 'becomes an IDV agency' do + expect(subject).to match_array(initial_report) + + create(:service_provider, :active, :idv, agency: agency) + + # The report gets memoized, so we need to reconstruct it here: + new_report = described_class.new(report_date) + expect(new_report.agency_and_sp_report).to match_array(updated_report) + end + end + + context 'when adding an IDV SP' do + let!(:idv_sp) { create(:service_provider, :idv, :active) } + + let(:expected_report) do + [ + header_row, + ['Auth', 0, 0], + ['IDV', 1, 1], + ] + end + + it 'counts the SP and its Agency as IDV' do + expect(subject).to match_array(expected_report) + end + end + end +end diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb index abe9483a93a..23926d4f10b 100644 --- a/spec/support/features/idv_step_helper.rb +++ b/spec/support/features/idv_step_helper.rb @@ -127,6 +127,18 @@ def expect_step_indicator_current_step(text) expect(page).to have_css('.step-indicator__step--current', text: text, wait: 5) end + def complete_idv_steps_before_address(user = user_with_2fa) + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + # prepare page + complete_prepare_step(user) + # location page + complete_location_step(user) + # state ID page + fill_out_state_id_form_ok(same_address_as_id: false) + click_idv_continue + end + def complete_idv_steps_before_ssn(user = user_with_2fa) sign_in_and_2fa_user(user) begin_in_person_proofing(user) diff --git a/spec/support/idv_examples/gpo_otp_verification.rb b/spec/support/idv_examples/verification_code_entry.rb similarity index 92% rename from spec/support/idv_examples/gpo_otp_verification.rb rename to spec/support/idv_examples/verification_code_entry.rb index 62cfe696866..a5316310296 100644 --- a/spec/support/idv_examples/gpo_otp_verification.rb +++ b/spec/support/idv_examples/verification_code_entry.rb @@ -1,7 +1,7 @@ -RSpec.shared_examples 'gpo otp verification' do +RSpec.shared_examples 'verification code entry' do include IdvStepHelper - it 'prompts for one-time code at sign in' do + it 'prompts for verification code at sign in' do sign_in_live_with_2fa(user) expect(current_path).to eq idv_verify_by_mail_enter_code_path @@ -26,7 +26,7 @@ expect(page).to_not have_content(t('account.index.verification.reactivate_button')) end - it 'renders an error for an expired GPO OTP' do + it 'renders an error for an expired verification code' do sign_in_live_with_2fa(user) gpo_confirmation_code.update( diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb index 296212cd7c3..5e2eca1095b 100644 --- a/spec/support/matchers/accessibility.rb +++ b/spec/support/matchers/accessibility.rb @@ -211,28 +211,10 @@ def computed_name(element) end end -RSpec::Matchers.define :be_uniquely_titled do - # Attempts to validate conformance to WCAG Success Criteria 2.4.2: Page Titled - # - # Visiting a page with the default app name title is considered a failure, and should be resolved - # by providing a distinct description for the page using the `:title` content block. - # - # https://www.w3.org/WAI/WCAG21/Understanding/page-titled.html - - match do |page| - page.title.present? && page.title.strip != APP_NAME - end - - failure_message do |page| - "Page '#{page.current_path}' should have a unique and descriptive title. Found '#{page.title}'." - end -end - def expect_page_to_have_no_accessibility_violations(page, validate_markup: true) expect(page).to be_axe_clean.according_to :section508, :"best-practice", :wcag21aa expect(page).to have_valid_idrefs expect(page).to label_required_fields - expect(page).to be_uniquely_titled expect(page).to have_valid_markup if validate_markup end diff --git a/spec/views/layouts/application.html.erb_spec.rb b/spec/views/layouts/application.html.erb_spec.rb index f82c57656ff..5b905acf562 100644 --- a/spec/views/layouts/application.html.erb_spec.rb +++ b/spec/views/layouts/application.html.erb_spec.rb @@ -3,6 +3,8 @@ RSpec.describe 'layouts/application.html.erb' do include Devise::Test::ControllerHelpers + let(:title_content) { 'Example' } + before do allow(view).to receive(:user_fully_authenticated?).and_return(true) allow(view).to receive(:decorated_sp_session).and_return( @@ -17,6 +19,7 @@ allow(view).to receive(:current_user).and_return(User.new) controller.request.path_parameters[:controller] = 'users/sessions' controller.request.path_parameters[:action] = 'new' + view.title(title_content) if title_content end context 'no content for nav present' do @@ -67,32 +70,33 @@ end context '' do - context 'with a page title added' do - it 'does not double-escape HTML in the title tag' do - view.title("Something with 'single quotes'") - - render + context 'without title' do + let(:title_content) { nil } - doc = Nokogiri::HTML(rendered) - expect(doc.at_css('title').text).to eq("Something with 'single quotes' | #{APP_NAME}") + it 'raises an error' do + expect { render }.to raise_error 'Missing title' end + end - it 'properly works with > in the title tag' do - view.title('Symbols <>') + context 'with escapable html' do + let(:title_content) { "Something with 'single quotes'" } + it 'does not double-escape HTML' do render doc = Nokogiri::HTML(rendered) - expect(doc.at_css('title').text).to eq("Symbols <> | #{APP_NAME}") + expect(doc.at_css('title').text).to eq("Something with 'single quotes' | #{APP_NAME}") end end - context 'without a page title added' do - it 'should only have Login.gov as title' do + context 'with html opening or closing syntax' do + let(:title_content) { 'Symbols <>' } + + it 'properly encodes text' do render doc = Nokogiri::HTML(rendered) - expect(doc.at_css('title').text).to eq(APP_NAME) + expect(doc.at_css('title').text).to eq("Symbols <> | #{APP_NAME}") end end end diff --git a/yarn.lock b/yarn.lock index f774e2deb8b..2631d8b0c73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4570,10 +4570,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.48: - version "1.10.48" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.48.tgz#3c426b4aa21dfe3210bfbda47d208acffa3631bf" - integrity sha512-Vvcgt4+o8+puIBJZLdMshPYx9nRN3/kTT7HPtOyfYrSQuN9PGBF1KUv0g07fjNzt4E4GuA7FnsLb+WeAMzyRQg== +libphonenumber-js@^1.10.49: + version "1.10.49" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz#c871661c62452348d228c96425f75ddf7e10f05a" + integrity sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ== lightningcss-darwin-arm64@1.22.0: version "1.22.0"