diff --git a/app/controllers/accounts/connected_accounts/selected_email_controller.rb b/app/controllers/accounts/connected_accounts/selected_email_controller.rb index 9add839cf12..f782b052491 100644 --- a/app/controllers/accounts/connected_accounts/selected_email_controller.rb +++ b/app/controllers/accounts/connected_accounts/selected_email_controller.rb @@ -12,6 +12,7 @@ class SelectedEmailController < ApplicationController def edit @identity = identity @select_email_form = build_select_email_form + @can_add_email = EmailPolicy.new(current_user).can_add_email? analytics.sp_select_email_visited end diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index e02aa9b69b2..edcfaa0f4d8 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -8,7 +8,6 @@ module DocumentCaptureConcern def handle_stored_result(user: current_user, store_in_session: true) if stored_result&.success? && selfie_requirement_met? - save_proofing_components(user) extract_pii_from_doc(user, store_in_session: store_in_session) flash[:success] = t('doc_auth.headings.capture_complete') successful_response @@ -18,16 +17,6 @@ def handle_stored_result(user: current_user, store_in_session: true) end end - def save_proofing_components(user) - return unless user - - component_attributes = { - document_check: doc_auth_vendor, - document_type: 'state_id', - } - ProofingComponent.create_or_find_by(user: user).update(component_attributes) - end - def successful_response FormResponse.new(success: true) end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index f3c0ac15f3a..fc71a949363 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -267,26 +267,11 @@ def summarize_result_and_rate_limit(summary_result) proofing_results_exception = summary_result.extra.dig(:proofing_results, :exception) resolution_rate_limiter.increment! if proofing_results_exception.blank? - if summary_result.success? - add_proofing_components(summary_result) - else + if !summary_result.success? idv_failure(summary_result) end end - def add_proofing_components(summary_result) - ProofingComponent.create_or_find_by(user: current_user).update( - resolution_check: Idp::Constants::Vendors::LEXIS_NEXIS, - source_check: summary_result.extra.dig( - :proofing_results, - :context, - :stages, - :state_id, - :vendor_name, - ), - ) - end - def load_async_state dcs_uuid = idv_session.verify_info_step_document_capture_session_uuid dcs = DocumentCaptureSession.find_by(uuid: dcs_uuid) diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index b7799d71181..e78689f9488 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -48,8 +48,6 @@ def update_tracking log_letter_requested_analytics(resend: false) create_user_event(:gpo_mail_sent, current_user) - - ProofingComponent.find_or_create_by(user: current_user).update(address_check: 'gpo_letter') end def confirm_mail_not_rate_limited diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index 36412215794..35c8366801e 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -63,13 +63,9 @@ def update sponsor_id: enrollment_sponsor_id, ) - add_proofing_component - render json: { success: true }, status: :ok end - private - def idv_session if user_session && current_user @idv_session ||= Idv::Session.new( @@ -80,6 +76,8 @@ def idv_session end end + private + def document_capture_session if idv_session&.document_capture_session_uuid # standard flow DocumentCaptureSession.find_by(uuid: idv_session.document_capture_session_uuid) @@ -92,12 +90,6 @@ def proofer @proofer ||= EnrollmentHelper.usps_proofer end - def add_proofing_component - ProofingComponent. - create_or_find_by(user: current_or_hybrid_user). - update(document_check: Idp::Constants::Vendors::USPS) - end - def localized_locations(locations) return nil if locations.nil? locations.map do |location| diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 2c16e0d1e6f..93a8137bc86 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -65,7 +65,6 @@ def analytics_arguments end def handle_document_verification_success - save_proofing_components(current_user) extract_pii_from_doc(current_user, store_in_session: true) idv_session.flow_path = 'hybrid' end diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 7cccf2d8924..358b1f97fd0 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -29,7 +29,6 @@ def show if pii_is_missing? redirect_to_retrieve_pii else - add_proofing_component finish_idv_session end end @@ -78,10 +77,6 @@ def next_step end end - def add_proofing_component - ProofingComponent.find_or_create_by(user: current_user).update(verified_at: Time.zone.now) - end - def finish_idv_session @code = personal_key @personal_key_generated_at = current_user.personal_key_generated_at diff --git a/app/controllers/redirect/redirect_controller.rb b/app/controllers/redirect/redirect_controller.rb index c4e90625644..d63025667e9 100644 --- a/app/controllers/redirect/redirect_controller.rb +++ b/app/controllers/redirect/redirect_controller.rb @@ -17,14 +17,8 @@ def partner_params }.compact end - def redirect_to_and_log(url, event: nil, tracker_method: analytics.method(:external_redirect)) - if event - # Once all events have been moved to tracker methods, we can remove the event: param - analytics.track_event(event, redirect_url: url, **location_params) - else - tracker_method.call(redirect_url: url, **location_params) - end - + def redirect_to_and_log(url, tracker_method: analytics.method(:external_redirect)) + tracker_method.call(redirect_url: url, **location_params) redirect_url = UriService.add_params(url, partner_params) redirect_to(redirect_url, allow_other_host: true) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb index 41bc0258206..2d691d85d8a 100644 --- a/app/controllers/sign_up/select_email_controller.rb +++ b/app/controllers/sign_up/select_email_controller.rb @@ -14,6 +14,7 @@ def show @user_emails = user_emails @last_sign_in_email_address = last_email @select_email_form = build_select_email_form + @can_add_email = EmailPolicy.new(current_user).can_add_email? analytics.sp_select_email_visited(needs_completion_screen_reason:) end diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 0d0354da7d1..0028767330d 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -38,7 +38,7 @@ def fetch_results if IdentityConfig.store.ruby_workers_idv_enabled SocureDocvResultsJob.perform_later(document_capture_session_uuid: dcs.uuid) else - SocureDocvResultsJob.perform_now(document_capture_session_uuid: dcs.uuid) + SocureDocvResultsJob.perform_now(document_capture_session_uuid: dcs.uuid, async: false) end end diff --git a/app/forms/event_disavowal/password_reset_from_disavowal_form.rb b/app/forms/event_disavowal/password_reset_from_disavowal_form.rb index 06df1ca4e2c..9f418501e0f 100644 --- a/app/forms/event_disavowal/password_reset_from_disavowal_form.rb +++ b/app/forms/event_disavowal/password_reset_from_disavowal_form.rb @@ -36,7 +36,6 @@ def mark_profile_inactive user.active_profile&.deactivate(:password_reset) Funnel::DocAuth::ResetSteps.call(@user.id) - user.proofing_component&.destroy end def extra_analytics_attributes diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index adcf084577e..d07ffa8ebdd 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -65,7 +65,6 @@ def mark_profile_inactive active_profile.deactivate(:password_reset) Funnel::DocAuth::ResetSteps.call(user.id) - user.proofing_component&.destroy end # It is possible for an account that is resetting their password to be "invalid". diff --git a/app/javascript/packages/webauthn/is-expected-error.spec.ts b/app/javascript/packages/webauthn/is-expected-error.spec.ts index 58834e29d05..725753896f4 100644 --- a/app/javascript/packages/webauthn/is-expected-error.spec.ts +++ b/app/javascript/packages/webauthn/is-expected-error.spec.ts @@ -36,4 +36,34 @@ describe('isExpectedWebauthnError', () => { expect(result).to.be.true(); }); + + it('returns true for a NotReadableError Android credential manager incompatibility', () => { + const error = new DOMException( + 'An unknown error occurred while talking to the credential manager.', + 'NotReadableError', + ); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.true(); + }); + + it('returns false for NotSupportedError when not during verification', () => { + const error = new DOMException( + 'The user agent does not support public key credentials.', + 'NotSupportedError', + ); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.false(); + }); + + it('returns true for NotSupportedError during verification', () => { + const error = new DOMException( + 'The user agent does not support public key credentials.', + 'NotSupportedError', + ); + const result = isExpectedWebauthnError(error, { isVerifying: true }); + + expect(result).to.be.true(); + }); }); diff --git a/app/javascript/packages/webauthn/is-expected-error.ts b/app/javascript/packages/webauthn/is-expected-error.ts index 7ee1c48babc..9e889cbd037 100644 --- a/app/javascript/packages/webauthn/is-expected-error.ts +++ b/app/javascript/packages/webauthn/is-expected-error.ts @@ -1,19 +1,34 @@ import isUserVerificationScreenLockError from './is-user-verification-screen-lock-error'; /** - * Set of expected DOM exceptions, which occur based on some user behavior that is not noteworthy: - * - * - Declining permissions - * - Timeout due to inactivity - * - Invalid state such as duplicate key enrollment - * - * @see https://webidl.spec.whatwg.org/#idl-DOMException + * Functions to test whether an error is expected and should not be logged for further analysis. */ -const EXPECTED_DOM_EXCEPTIONS: Set = new Set([ - 'NotAllowedError', - 'TimeoutError', - 'InvalidStateError', -]); +const EXPECTED_ERRORS: Array<(error: Error, options: IsExpectedErrorOptions) => boolean> = [ + // A user who is unable to complete due to following DOMException reasons is not noteworthy: + // + // - Declining permissions + // - Timeout due to inactivity + // - Invalid state such as duplicate key enrollment + (error) => + error.name === 'NotAllowedError' || + error.name === 'TimeoutError' || + error.name === 'InvalidStateError', + // Some indication of incompatibilities on specific Android devices, either phone itself or + // through credential manager. + // + // See: https://community.bitwarden.com/t/android-mobile-yubikey-5-nfc-webauth/51732 + // See: https://www.reddit.com/r/GooglePixel/comments/17enqf3/pixel_7_pro_unable_to_setup_passkeys/ + (error) => + error.name === 'NotReadableError' && + error.message === 'An unknown error occurred while talking to the credential manager.', + // A user can choose to authenticate with Face or Touch Unlock from another device from what + // they set up from, which may not necessarily support platform authenticators. + (error, { isVerifying }) => isVerifying && isUserVerificationScreenLockError(error), + (error, { isVerifying }) => + isVerifying && + error.name === 'NotSupportedError' && + error.message === 'The user agent does not support public key credentials.', +]; interface IsExpectedErrorOptions { /** @@ -22,14 +37,9 @@ interface IsExpectedErrorOptions { isVerifying: boolean; } -function isExpectedWebauthnError( +const isExpectedWebauthnError = ( error: Error, - { isVerifying }: Partial = {}, -): boolean { - return ( - (error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name)) || - (!!isVerifying && isUserVerificationScreenLockError(error)) - ); -} + { isVerifying = false }: Partial = {}, +): boolean => EXPECTED_ERRORS.some((isExpected) => isExpected(error, { isVerifying })); export default isExpectedWebauthnError; diff --git a/app/jobs/data_warehouse/base_job.rb b/app/jobs/data_warehouse/base_job.rb index 5db5f079a27..85ee26caa3d 100644 --- a/app/jobs/data_warehouse/base_job.rb +++ b/app/jobs/data_warehouse/base_job.rb @@ -42,5 +42,9 @@ def upload_file_to_s3_bucket(path:, body:, content_type:, bucket: bucket_name) logger.debug("#{class_name}: upload completed to #{url}") url end + + def data_warehouse_disabled? + !IdentityConfig.store.data_warehouse_enabled + end end end diff --git a/app/jobs/data_warehouse/table_summary_stats_export_job.rb b/app/jobs/data_warehouse/table_summary_stats_export_job.rb index 7fef9cabee5..44622a17620 100644 --- a/app/jobs/data_warehouse/table_summary_stats_export_job.rb +++ b/app/jobs/data_warehouse/table_summary_stats_export_job.rb @@ -5,6 +5,8 @@ class TableSummaryStatsExportJob < BaseJob REPORT_NAME = 'table_summary_stats' def perform(timestamp) + return if data_warehouse_disabled? + data = fetch_table_max_ids_and_counts(timestamp) upload_to_s3(data, timestamp) end diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index b8cd4e08a95..cf5b3b9cbe3 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -74,7 +74,7 @@ def perform( timing: timer.results, ) - if IdentityConfig.store.idv_socure_shadow_mode_enabled + if use_shadow_mode?(user:) SocureShadowModeProofingJob.perform_later( document_capture_session_result_id: document_capture_session&.result_id, encrypted_arguments:, @@ -85,6 +85,17 @@ def perform( end end + def use_shadow_mode?(user:) + IdentityConfig.store.idv_socure_shadow_mode_enabled && + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session: nil, + ) == :shadow_mode_enabled + end + private # @return [CallbackLogData] @@ -108,7 +119,6 @@ def make_vendor_proofing_requests( ) log_threatmetrix_info(result.device_profiling_result, user) - add_threatmetrix_proofing_component(user.id, result.device_profiling_result) if user.present? CallbackLogData.new( device_profiling_success: result.device_profiling_result.success?, @@ -139,11 +149,4 @@ def logger_info_hash(hash) def progressive_proofer @progressive_proofer ||= Proofing::Resolution::ProgressiveProofer.new end - - def add_threatmetrix_proofing_component(user_id, threatmetrix_result) - ProofingComponent. - create_or_find_by(user_id: user_id). - update(threatmetrix: FeatureManagement.proofing_device_profiling_collecting_enabled?, - threatmetrix_review_status: threatmetrix_result.review_status) - end end diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index ad3fa7bca04..25729078575 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -3,35 +3,48 @@ class SocureDocvResultsJob < ApplicationJob queue_as :high_socure_docv - attr_reader :document_capture_session_uuid + attr_reader :document_capture_session_uuid, :async # @param [String] document_capture_session_uuid - def perform(document_capture_session_uuid:) + def perform(document_capture_session_uuid:, async: true) @document_capture_session_uuid = document_capture_session_uuid + @async = async - dcs = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) - raise "DocumentCaptureSession not found: #{document_capture_session_uuid}" if !dcs + raise "DocumentCaptureSession not found: #{document_capture_session_uuid}" unless + document_capture_session - # analytics = create_analytics( - # user: dcs.user, - # service_provider_issuer: dcs.issuer, - # ) - - result = socure_document_verification_result - dcs.store_result_from_response(result) + timer = JobHelpers::Timer.new + response = timer.time('vendor_request') do + socure_document_verification_result + end + log_verification_request( + docv_result_response: response, + vendor_request_time_in_ms: timer.results['vendor_request'], + ) + document_capture_session.store_result_from_response(response) end private - def create_analytics( - user:, - service_provider_issuer: - ) - Analytics.new( - user:, + def analytics + @analytics ||= Analytics.new( + user: document_capture_session.user, request: nil, - sp: service_provider_issuer, session: {}, + sp: document_capture_session.issuer, + ) + end + + def log_verification_request(docv_result_response:, vendor_request_time_in_ms:) + analytics.idv_socure_verification_data_requested( + **docv_result_response.to_h.merge( + docv_transaction_token: document_capture_session.socure_docv_transaction_token, + submit_attempts: rate_limiter&.attempts, + remaining_submit_attempts: rate_limiter&.remaining_count, + vendor_request_time_in_ms:, + async:, + ).except(:attention_with_barcode, :selfie_live, :selfie_quality_good, + :selfie_status), ) end @@ -40,4 +53,16 @@ def socure_document_verification_result document_capture_session_uuid:, ).fetch end + + def document_capture_session + @document_capture_session ||= + DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) + end + + def rate_limiter + @rate_limiter ||= RateLimiter.new( + user: document_capture_session.user, + rate_limit_type: :idv_doc_auth, + ) + end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ca8c1f2f0a5..32678fad843 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -67,7 +67,8 @@ def reset_password_instructions(token:, request_id:) @token = token @request_id = request_id @gpo_verification_pending_profile = user.gpo_verification_pending_profile? - @hide_title = @gpo_verification_pending_profile + @in_person_verification_pending_profile = user.in_person_pending_profile? + @hide_title = @gpo_verification_pending_profile || @in_person_verification_pending_profile mail(to: email_address.email, subject: t('user_mailer.reset_password_instructions.subject')) end end diff --git a/app/models/profile.rb b/app/models/profile.rb index be986e03cf4..2052eb33eaf 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class Profile < ApplicationRecord + # IDV levels equivalent to facial match FACIAL_MATCH_IDV_LEVELS = %w[unsupervised_with_selfie in_person].to_set.freeze + # Facial match through IAL2 opt-in flow + FACIAL_MATCH_OPT_IN = %w[unsupervised_with_selfie].to_set.freeze belongs_to :user # rubocop:disable Rails/InverseOf @@ -52,6 +55,14 @@ def self.verified where.not(verified_at: nil) end + def self.facial_match + where(idv_level: FACIAL_MATCH_IDV_LEVELS) + end + + def self.facial_match_opt_in + where(idv_level: FACIAL_MATCH_OPT_IN) + end + def self.fraud_rejection where.not(fraud_rejection_at: nil) end diff --git a/app/models/proofing_component.rb b/app/models/proofing_component.rb deleted file mode 100644 index 55ef4d750f0..00000000000 --- a/app/models/proofing_component.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ProofingComponent < ApplicationRecord - belongs_to :user -end diff --git a/app/models/user.rb b/app/models/user.rb index 5d53cc38dac..88c89cc9daf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -44,7 +44,6 @@ class User < ApplicationRecord has_many :backup_code_configurations, dependent: :destroy has_many :document_capture_sessions, dependent: :destroy has_one :registration_log, dependent: :destroy - has_one :proofing_component, dependent: :destroy has_many :service_providers, through: :identities, source: :service_provider_record diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 2bc84a7d2cb..980d83a391f 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4708,6 +4708,101 @@ def idv_socure_shadow_mode_proofing_result_missing(**extra) track_event(:idv_socure_shadow_mode_proofing_result_missing, **extra) end + # @param [Boolean] success Whether form validation was successful + # @param [Hash] errors Errors resulting from form validation + # @param [String] exception + # @param [Boolean] billed + # @param [String] docv_transaction_token socure transaction token + # @param [Hash] customer_profile socure customer profile + # @param [String] reference_id socure interal id for transaction + # @param [Hash] reason_codes socure internal reason codes for accept reject decision + # @param [Hash] document_type type of socument submitted (Drivers Licenese, etc.) + # @param [Hash] decision accept or reject of given ID + # @param [String] user_id internal id of socure user + # @param [String] state state of ID + # @param [String] state_id_type type of state issued ID + # @param [Boolean] async whether or not this worker is running asynchronously + # @param [Integer] submit_attempts Times that user has tried submitting (previously called + # "attempts") + # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") + # @param ["hybrid","standard"] flow_path Document capture user flow + # @param [Float] vendor_request_time_in_ms Time it took to upload images & get a response. + # @param [Boolean] doc_type_supported + # @param [Boolean] doc_auth_success + # @param [Boolean] liveness_checking_required Whether or not the selfie is required + # @param [Boolean] liveness_enabled Whether or not the selfie result is included in response + # @param [String] vendor which 2rd party we are using for doc auth + # @param [Boolean] address_line2_present wether or not we have an address that uses the 2nd line + # @param [String] zip_code zip code from state issued ID + # @param [String] birth_year Birth year from document + # @param [Integer] issue_year Year document was issued + # @param [Boolean] biometric_comparison_required does doc auth require biometirc + # The request for socure verification was sent + def idv_socure_verification_data_requested( + success:, + errors:, + async:, + reference_id:, + reason_codes:, + document_type:, + decision:, + state:, + state_id_type:, + submit_attempts:, + remaining_submit_attempts:, + liveness_checking_required:, + issue_year:, + vendor_request_time_in_ms:, + doc_type_supported:, + doc_auth_success:, + vendor:, + address_line2_present:, + zip_code:, + birth_year:, + liveness_enabled:, + biometric_comparison_required:, + customer_profile: nil, + docv_transaction_token: nil, + user_id: nil, + exception: nil, + flow_path: nil, + billed: nil, + **extra + ) + track_event( + :idv_socure_verification_data_requested, + success:, + errors:, + exception:, + billed:, + docv_transaction_token:, + customer_profile:, + reference_id:, + reason_codes:, + document_type:, + decision:, + user_id:, + state:, + state_id_type:, + async:, + submit_attempts:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + vendor_request_time_in_ms:, + doc_type_supported:, + doc_auth_success:, + vendor:, + address_line2_present:, + zip_code:, + birth_year:, + issue_year:, + liveness_enabled:, + biometric_comparison_required:, + **extra, + ) + end + # @param [String] step # @param [String] location # @param [Hash,nil] proofing_components User's current proofing components diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb index 5aa9439d645..0c6e5b2556b 100644 --- a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb +++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb @@ -27,10 +27,11 @@ def read_pii(true_id_product) state_id_type_slug = id_auth_field_data['Fields_DocumentClassName'] state_id_type = DocAuth::Response::ID_TYPE_SLUGS[state_id_type_slug] - Pii::StateId.new( + state_id_data = Pii::StateId.new( first_name: id_auth_field_data['Fields_FirstName'], last_name: id_auth_field_data['Fields_Surname'], middle_name: id_auth_field_data['Fields_MiddleName'], + name_suffix: nil, address1: id_auth_field_data['Fields_AddressLine1'], address2: id_auth_field_data['Fields_AddressLine2'], city: id_auth_field_data['Fields_City'], @@ -41,6 +42,10 @@ def read_pii(true_id_product) month: id_auth_field_data['Fields_DOB_Month'], day: id_auth_field_data['Fields_DOB_Day'], ), + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: parse_date( year: id_auth_field_data['Fields_ExpirationDate_Year'], month: id_auth_field_data['Fields_ExpirationDate_Month'], @@ -56,6 +61,16 @@ def read_pii(true_id_product) state_id_type: state_id_type, issuing_country_code: id_auth_field_data['Fields_CountryCode'], ) + + if IdentityConfig.store.doc_auth_read_additional_pii_attributes_enabled + state_id_data = state_id_data.with( + name_suffix: id_auth_field_data['Fields_NameSuffix'], + sex: parse_sex_value(id_auth_field_data['Fields_Sex']), + height: parse_height_value(id_auth_field_data['Fields_Height']), + ) + end + + state_id_data end def parse_date(year:, month:, day:) @@ -67,6 +82,36 @@ def parse_date(year:, month:, day:) Rails.logger.info(message) nil end + + def parse_sex_value(sex_attribute) + # A value of "non-binary" or "not-specified" may appear on a document. However, at this time + # the DLDV `PersonSexCode` input can only process values that correspond to "male" or + # "female". + # + # From the DLDV User Guide Version 2.1 - 28: + # + # Since 2017, a growing number of states have allowed a person to select "not specified" + # or "non-binary" for their sex on the application for a credential. While Male and + # Female can be verified, the non-binary value cannot be verified at this time. + # + # This code will return `nil` for those cases with the intent that they will not be verified + # against the DLDV where they will not be recognized + # + case sex_attribute&.upcase + when 'M' + 'male' + when 'F' + 'female' + end + end + + def parse_height_value(height_attribute) + height_match_data = height_attribute&.match(/(?\d)'(?\d{1,2})"/) + + return unless height_match_data + + height_match_data[:feet].to_i * 12 + height_match_data[:inches].to_i + end end end end diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index aaf4413da27..d2beacb7966 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -30,9 +30,13 @@ class DocvResultResponse < DocAuth::Response document_number: %w[documentVerification documentData documentNumber], issue_date: %w[documentVerification documentData issueDate], expiration_date: %w[documentVerification documentData expirationDate], + customer_profile: %w[customerProfile], + socure_customer_user_id: %w[customerProfile customerUserId], + socure_user_id: %w[customerProfile userId], }.freeze - def initialize(http_response:, biometric_comparison_required: false) + def initialize(http_response:, + biometric_comparison_required: false) @http_response = http_response @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -63,6 +67,28 @@ def selfie_status :not_processed end + def extra_attributes + { + reference_id: get_data(DATA_PATHS[:reference_id]), + decision: get_data(DATA_PATHS[:decision]), + biometric_comparison_required: biometric_comparison_required, + customer_profile: get_data(DATA_PATHS[:customer_profile]), + reason_codes: get_data(DATA_PATHS[:reason_codes]), + document_type: get_data(DATA_PATHS[:document_type]), + state: state, + state_id_type: state_id_type, + flow_path: nil, + liveness_checking_required: @biometric_comparison_required, + issue_year: state_id_issued&.year, + doc_auth_success: successful_result?, + vendor: 'Socure', + address_line2_present: address2.present?, + zip_code: zipcode, + birth_year: dob&.year, + liveness_enabled: @biometric_comparison_required, + } + end + private def successful_result? @@ -77,27 +103,24 @@ def error_messages } end - def extra_attributes - { - reference_id: get_data(DATA_PATHS[:reference_id]), - decision: get_data(DATA_PATHS[:decision]), - biometric_comparison_required: biometric_comparison_required, - } - end - def read_pii Pii::StateId.new( first_name: get_data(DATA_PATHS[:first_name]), middle_name: get_data(DATA_PATHS[:middle_name]), last_name: get_data(DATA_PATHS[:last_name]), + name_suffix: nil, address1: get_data(DATA_PATHS[:address1]), - address2: get_data(DATA_PATHS[:address2]), + address2:, city: get_data(DATA_PATHS[:city]), state: get_data(DATA_PATHS[:state]), zipcode: get_data(DATA_PATHS[:zipcode]), dob: parse_date(get_data(DATA_PATHS[:dob])), + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_number: get_data(DATA_PATHS[:document_number]), - state_id_issued: parse_date(get_data(DATA_PATHS[:issue_date])), + state_id_issued:, state_id_expiration: parse_date(get_data(DATA_PATHS[:expiration_date])), state_id_type: state_id_type, state_id_jurisdiction: get_data(DATA_PATHS[:issuing_state]), @@ -119,11 +142,31 @@ def parsed_response_body end end + def state + get_data(DATA_PATHS[:state]) + end + + def zipcode + get_data(DATA_PATHS[:zipcode]) + end + + def state_id_issued + parse_date(get_data(DATA_PATHS[:issue_date])) + end + def state_id_type type = get_data(DATA_PATHS[:id_type]) type&.gsub(/\W/, '')&.underscore end + def dob + parse_date(get_data(DATA_PATHS[:dob])) + end + + def address2 + get_data(DATA_PATHS[:address2]) + end + def parse_date(date_string) Date.parse(date_string) rescue ArgumentError, TypeError diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 6f67339f580..810e933438d 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -31,6 +31,7 @@ module AnalyticsEventsEnhancer idv_doc_auth_ssn_visited idv_doc_auth_submitted_image_upload_form idv_doc_auth_submitted_image_upload_vendor + idv_socure_verification_data_requested idv_doc_auth_submitted_pii_validation idv_doc_auth_verify_proofing_results idv_doc_auth_verify_submitted diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 0ed264ffefd..eb13660adce 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -132,9 +132,6 @@ def failed_due_to_timeout_or_exception? def update_idv_session idv_session.applicant = applicant idv_session.mark_phone_step_started! - - ProofingComponent.find_or_create_by(user: idv_session.current_user). - update(address_check: 'lexis_nexis_address') end def start_phone_confirmation_session diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 4f4683997de..071ce403a80 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -26,7 +26,6 @@ module Idv # @attr resolution_successful [Boolean, nil] # @attr selfie_check_performed [Boolean, nil] # @attr selfie_check_required [Boolean, nil] - # @attr skip_doc_auth [Boolean, nil] # @attr skip_doc_auth_from_handoff [Boolean, nil] # @attr skip_doc_auth_from_how_to_verify [Boolean, nil] # @attr skip_hybrid_handoff [Boolean, nil] @@ -69,7 +68,6 @@ class Session resolution_successful selfie_check_performed selfie_check_required - skip_doc_auth skip_doc_auth_from_handoff skip_doc_auth_from_how_to_verify skip_hybrid_handoff @@ -230,7 +228,8 @@ def pii_from_doc=(new_pii_from_doc) def pii_from_doc return nil if session[:pii_from_doc].blank? - Pii::StateId.new(**session[:pii_from_doc].slice(*Pii::StateId.members)) + state_id_data = Pii::StateId.members.index_with { |key| session[:pii_from_doc][key] } + Pii::StateId.new(**state_id_data) end def updated_user_address=(updated_user_address) diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 52306235d87..09434827548 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -9,21 +9,6 @@ def initialize(flow) private - def save_proofing_components - return unless current_user - - doc_auth_vendor = DocAuthRouter.doc_auth_vendor( - discriminator: flow_session[document_capture_session_uuid_key], - analytics: @flow.analytics, - ) - - component_attributes = { - document_check: doc_auth_vendor, - document_type: 'state_id', - } - ProofingComponent.create_or_find_by(user: current_user).update(component_attributes) - end - def user_id_from_token flow_session[:doc_capture_user_id] end diff --git a/app/services/pii/state_id.rb b/app/services/pii/state_id.rb index 713766d68ee..60ddbd24988 100644 --- a/app/services/pii/state_id.rb +++ b/app/services/pii/state_id.rb @@ -6,17 +6,22 @@ module Pii :first_name, :last_name, :middle_name, + :name_suffix, :address1, :address2, :city, :state, + :zipcode, :dob, + :sex, + :height, + :weight, + :eye_color, :state_id_expiration, :state_id_issued, :state_id_jurisdiction, :state_id_number, :state_id_type, - :zipcode, :issuing_country_code, ) end diff --git a/app/services/proofing/aamva/applicant.rb b/app/services/proofing/aamva/applicant.rb index d704cfe8708..a0e697b0e8c 100644 --- a/app/services/proofing/aamva/applicant.rb +++ b/app/services/proofing/aamva/applicant.rb @@ -8,7 +8,13 @@ module Aamva :uuid, :first_name, :last_name, + :middle_name, + :name_suffix, :dob, + :height, + :sex, + :weight, + :eye_color, :state_id_data, :address1, :address2, @@ -32,7 +38,13 @@ def self.from_proofer_applicant(applicant) uuid: applicant[:uuid], first_name: applicant[:first_name], last_name: applicant[:last_name], + middle_name: applicant[:middle_name], + name_suffix: applicant[:name_suffix], dob: format_dob(applicant[:dob]), + sex: applicant[:sex], + height: format_height(applicant[:height]), + weight: applicant[:weight], + eye_color: applicant[:eye_color], state_id_data: format_state_id_data(applicant), address1: applicant[:address1], address2: applicant[:address2], @@ -70,6 +82,16 @@ def self.from_proofer_applicant(applicant) state_id_expiration: applicant[:state_id_expiration], ) end + + private_class_method def self.format_height(height) + return if height.nil? + + # From the AAMVA DLDV guide regarding formatting the height: + # + # The height is provided in feet-inches (i.e. 5 foot 10 inches is presented as "510"). + # + [(height / 12).to_s, (height % 12).to_s].join('') + end end.freeze end end diff --git a/app/services/proofing/aamva/request/verification_request.rb b/app/services/proofing/aamva/request/verification_request.rb index 84e25e75b34..32e440407dc 100644 --- a/app/services/proofing/aamva/request/verification_request.rb +++ b/app/services/proofing/aamva/request/verification_request.rb @@ -63,6 +63,45 @@ def add_user_provided_data_to_body REXML::XPath.first(document, xpath).add_text(data) end + if IdentityConfig.store.aamva_send_middle_name + add_optional_element( + 'nc:PersonMiddleName', + value: applicant.middle_name, + document:, + inside: '//nc:PersonName', + ) + end + + add_optional_element( + 'nc:PersonNameSuffixText', + value: applicant.name_suffix, + document:, + inside: '//nc:PersonName', + ) + + add_optional_element( + 'aa:PersonHeightMeasure', + value: applicant.height, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'aa:PersonWeightMeasure', + value: applicant.weight, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_optional_element( + 'aa:PersonEyeColorCode', + value: applicant.eye_color, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + + add_sex_code(applicant.sex, document) + add_optional_element( 'nc:AddressDeliveryPointText', value: applicant.address2, @@ -114,6 +153,24 @@ def add_state_id_type(id_type, document) end end + def add_sex_code(sex_value, document) + sex_code = case sex_value + when 'male' + 1 + when 'female' + 2 + end + + if sex_code + add_optional_element( + 'aa:PersonSexCode', + value: sex_code, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + end + end + def add_optional_element(name, value:, document:, inside: nil, after: nil) return if value.blank? diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb index c0fc81d40d9..5c1a86d33d2 100644 --- a/app/services/proofing/aamva/response/verification_response.rb +++ b/app/services/proofing/aamva/response/verification_response.rb @@ -13,8 +13,14 @@ class VerificationResponse 'DriverLicenseNumberMatchIndicator' => :state_id_number, 'DocumentCategoryMatchIndicator' => :state_id_type, 'PersonBirthDateMatchIndicator' => :dob, + 'PersonHeightMatchIndicator' => :height, + 'PersonSexCodeMatchIndicator' => :sex, + 'PersonWeightMatchIndicator' => :weight, + 'PersonEyeColorMatchIndicator' => :eye_color, 'PersonLastNameExactMatchIndicator' => :last_name, 'PersonFirstNameExactMatchIndicator' => :first_name, + 'PersonMiddleNameExactMatchIndicator' => :middle_name, + 'PersonNameSuffixMatchIndicator' => :name_suffix, 'AddressLine1MatchIndicator' => :address1, 'AddressLine2MatchIndicator' => :address2, 'AddressCityMatchIndicator' => :city, diff --git a/app/services/proofing/resolution/result_adjudicator.rb b/app/services/proofing/resolution/result_adjudicator.rb index 09053d21216..908327ac585 100644 --- a/app/services/proofing/resolution/result_adjudicator.rb +++ b/app/services/proofing/resolution/result_adjudicator.rb @@ -123,6 +123,7 @@ def biographical_info StringRedacter.redact_alphanumeric(state_id_number) end { + birth_year: applicant_pii[:dob]&.to_date&.year, state: applicant_pii[:state], identity_doc_address_state: applicant_pii[:identity_doc_address_state], state_id_jurisdiction: applicant_pii[:state_id_jurisdiction], diff --git a/app/services/reporting/agency_and_sp_report.rb b/app/services/reporting/agency_and_sp_report.rb index 2ab63c8320d..d58744e3a31 100644 --- a/app/services/reporting/agency_and_sp_report.rb +++ b/app/services/reporting/agency_and_sp_report.rb @@ -48,12 +48,12 @@ def agency_and_sp_emailable_report end def active_agencies - @active_agencies ||= Agreements::PartnerAccountStatus.find_by(name: 'active'). - partner_accounts. - includes(:agency). - where('became_partner <= ?', report_date). - map(&:agency). - uniq + @active_agencies ||= Agency.joins(:partner_accounts). + where(partner_accounts: { + partner_account_status: Agreements::PartnerAccountStatus.find_by(name: 'active'), + became_partner: ..report_date, + }). + distinct end def service_providers @@ -67,11 +67,12 @@ def service_providers end def facial_match_issuers - @facial_match_issuers ||= Profile.where(active: true).where( - 'verified_at <= ?', - report_date.end_of_day, - ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS). - pluck(:initiating_service_provider_issuer).uniq + @facial_match_issuers ||= Reports::BaseReport.transaction_with_timeout do + Profile.active.facial_match_opt_in. + where('verified_at <= ?', report_date.end_of_day). + distinct. + pluck(:initiating_service_provider_issuer) + end end end end diff --git a/app/services/reporting/total_user_count_report.rb b/app/services/reporting/total_user_count_report.rb index 62ac028906d..b521f7a8f97 100644 --- a/app/services/reporting/total_user_count_report.rb +++ b/app/services/reporting/total_user_count_report.rb @@ -83,33 +83,30 @@ def total_user_count def verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where( - 'verified_at <= ?', - end_date, + Profile.active.where( + 'verified_at <= ?', end_date ).count - verified_facial_match_user_count end end def verified_facial_match_user_count @verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where( - 'verified_at <= ?', - end_date, - ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where( + 'verified_at <= ?', end_date + ).count end end def new_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month).count - + Profile.active.where(verified_at: current_month).count - new_verified_facial_match_user_count end end def new_verified_facial_match_user_count @new_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month). - where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where(verified_at: current_month).count end end @@ -121,15 +118,16 @@ def annual_total_user_count def annual_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date).count - + Profile.active.where(verified_at: annual_start_date..annual_end_date).count - annual_verified_facial_match_user_count end end def annual_verified_facial_match_user_count @annual_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date). - where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + Profile.active.facial_match_opt_in.where( + verified_at: annual_start_date..annual_end_date, + ).count end end diff --git a/app/views/accounts/connected_accounts/selected_email/edit.html.erb b/app/views/accounts/connected_accounts/selected_email/edit.html.erb index 3347832195c..c32e05f522b 100644 --- a/app/views/accounts/connected_accounts/selected_email/edit.html.erb +++ b/app/views/accounts/connected_accounts/selected_email/edit.html.erb @@ -33,13 +33,15 @@ <%= f.submit(t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1') %> <% end %> - <%= render ButtonComponent.new( - url: add_email_path(in_select_email_flow: true), - outline: true, - big: true, - wide: true, - class: 'margin-top-2', - ).with_content(t('account.index.email_add')) %> + <% if @can_add_email %> + <%= render ButtonComponent.new( + url: add_email_path(in_select_email_flow: true), + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + <% end %> <% c.with_footer { link_to t('forms.buttons.back'), account_connected_accounts_path } %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index ed505209855..4a967d2c97d 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -29,13 +29,15 @@ <%= f.submit t('help_text.requested_attributes.select_email_link'), class: 'margin-top-1' %> <% end %> - <%= render ButtonComponent.new( - url: add_email_path(in_select_email_flow: true), - outline: true, - big: true, - wide: true, - class: 'margin-top-2', - ).with_content(t('account.index.email_add')) %> + <% if @can_add_email %> + <%= render ButtonComponent.new( + url: add_email_path(in_select_email_flow: true), + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + <% end %> <%= render PageFooterComponent.new do %> <%= link_to t('forms.buttons.back'), sign_up_completed_path %> diff --git a/app/views/user_mailer/reset_password_instructions.html.erb b/app/views/user_mailer/reset_password_instructions.html.erb index 8a4616aa5e6..bed12ed7ed7 100644 --- a/app/views/user_mailer/reset_password_instructions.html.erb +++ b/app/views/user_mailer/reset_password_instructions.html.erb @@ -15,7 +15,15 @@

<%= @header || message.subject %>

- <% end %> +<% end %> + +<% if @in_person_verification_pending_profile %> + <%= render 'user_mailer/shared/in_person_warning_banner' %> +

+ <%= @header || message.subject %> +

+<% end %> +

<%= t( 'user_mailer.reset_password_instructions.header', diff --git a/app/views/user_mailer/shared/_in_person_warning_banner.html.erb b/app/views/user_mailer/shared/_in_person_warning_banner.html.erb new file mode 100644 index 00000000000..10fe28efa28 --- /dev/null +++ b/app/views/user_mailer/shared/_in_person_warning_banner.html.erb @@ -0,0 +1,11 @@ + + + + + + +
+ <%= image_tag('email/warning.png', width: 16, height: 14, alt: 'warning icon', style: 'margin-top: 5px;') %> + +

<%= t('user_mailer.reset_password_instructions.in_person_warning_description_html') %>

+
diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 00000000000..cb53ebe5f00 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/config/application.yml.default b/config/application.yml.default index 3e72fd32c6f..ea3d95f4d9e 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -20,6 +20,7 @@ aamva_cert_enabled: true aamva_private_key: '' aamva_public_key: '' aamva_send_id_type: true +aamva_send_middle_name: true aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url @@ -67,6 +68,7 @@ compromised_password_randomizer_value: 1000 country_phone_number_overrides: '{}' dashboard_api_token: '' dashboard_url: https://dashboard.demo.login.gov +data_warehouse_enabled: false database_advisory_locks_enabled: false database_host: '' database_name: '' @@ -102,6 +104,7 @@ doc_auth_error_sharpness_threshold: 40 doc_auth_max_attempts: 5 doc_auth_max_capture_attempts_before_native_camera: 3 doc_auth_max_submission_attempts_before_native_camera: 3 +doc_auth_read_additional_pii_attributes_enabled: false doc_auth_selfie_desktop_test_mode: false doc_auth_socure_wait_polling_refresh_max_seconds: 15 doc_auth_socure_wait_polling_timeout_minutes: 2 @@ -384,6 +387,7 @@ socure_docv_webhook_secret_key: '' socure_docv_webhook_secret_key_queue: '[]' socure_idplus_api_key: '' socure_idplus_base_url: '' +socure_idplus_shadow_mode_percent: 0 socure_idplus_timeout_in_seconds: 5 socure_reason_code_api_key: '' socure_reason_code_base_url: '' @@ -501,6 +505,7 @@ development: production: aamva_auth_url: 'https://authentication-cert.aamva.org/Authentication/Authenticate.svc' aamva_send_id_type: false + aamva_send_middle_name: false aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online' available_locales: 'en,es,fr' disable_email_sending: false diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index ea7288631d0..fff91a2dd65 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -96,4 +96,12 @@ def self.all IdentityConfig.store.recommend_webauthn_platform_for_sms_ab_test_authentication_percent, }, ).freeze + + SOCURE_IDV_SHADOW_MODE = AbTest.new( + experiment_name: 'Socure shadow mode', + should_log: ['IdV: doc auth verify proofing results'].to_set, + buckets: { + shadow_mode_enabled: IdentityConfig.store.socure_idplus_shadow_mode_percent, + }, + ).freeze end diff --git a/config/locales/en.yml b/config/locales/en.yml index f528ad32963..2ba1e98735e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1942,6 +1942,7 @@ user_mailer.reset_password_instructions.footer: This link expires in %{expires} user_mailer.reset_password_instructions.gpo_letter_description: If you reset your password, the verification code in your letter will no longer work and you’ll have to verify your identity again. user_mailer.reset_password_instructions.gpo_letter_header: Your letter is on the way user_mailer.reset_password_instructions.header: To finish resetting your password, please click the link below or copy and paste the entire link into your browser. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Reset your password user_mailer.reset_password_instructions.subject: Reset your password user_mailer.signup_with_your_email.help_html: If you did not request a new account or suspect an error, please visit the %{app_name_html} %{help_link_html} or %{contact_link_html}. diff --git a/config/locales/es.yml b/config/locales/es.yml index 3640e2aa666..d93a88dbcf3 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1954,6 +1954,7 @@ user_mailer.reset_password_instructions.footer: Este vínculo vence en %{expires user_mailer.reset_password_instructions.gpo_letter_description: Si restablece su contraseña, el código de verificación que recibió en su carta ya no funcionará y tendrá que volver a verificar su identidad. user_mailer.reset_password_instructions.gpo_letter_header: Su carta está en camino user_mailer.reset_password_instructions.header: Para terminar de restablecer su contraseña, haga clic en el enlace de abajo o copie y pegue el enlace completo en su navegador. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Restablezca su contraseña user_mailer.reset_password_instructions.subject: Restablezca su contraseña user_mailer.signup_with_your_email.help_html: Si usted no solicitó una cuenta nueva o sospecha que hubo un error, visite la %{help_link_html} de %{app_name_html} o %{contact_link_html}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 72ae134d8f5..e20fa43644b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1942,6 +1942,7 @@ user_mailer.reset_password_instructions.footer: Ce lien expire dans %{expires} h user_mailer.reset_password_instructions.gpo_letter_description: Si vous réinitialisez votre mot de passe, le code de vérification contenu dans votre lettre ne fonctionnera plus et vous devrez reconfirmer votre identité. user_mailer.reset_password_instructions.gpo_letter_header: Votre lettre est en route user_mailer.reset_password_instructions.header: Pour terminer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien ci-dessous ou copier et coller le lien complet dans votre navigateur. +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: Réinitialiser votre mot de passe user_mailer.reset_password_instructions.subject: Réinitialiser votre mot de passe user_mailer.signup_with_your_email.help_html: Si vous n’avez pas demandé un nouveau compte ou soupçonnez qu’une erreur s’est produite, veuillez visiter le %{help_link_html} de %{app_name_html} ou %{contact_link_html}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 62cd87be425..89061d000ad 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1955,6 +1955,7 @@ user_mailer.reset_password_instructions.footer: 这一链接 %{expires} 小时 user_mailer.reset_password_instructions.gpo_letter_description: 如果你重设密码,信件中的一次性代码就会失效,你需要再次验证身份。 user_mailer.reset_password_instructions.gpo_letter_header: 你的信件已寄出。 user_mailer.reset_password_instructions.header: 要完成重设密码,请点击下面的链接或把整个链接复制并黏贴进浏览器。 +user_mailer.reset_password_instructions.in_person_warning_description_html: If you reset your password now, your barcode will not work at the Post Office. You’ll have to restart the identity verification process from the beginning. user_mailer.reset_password_instructions.link_text: 重设你的密码 user_mailer.reset_password_instructions.subject: 重设你的密码 user_mailer.signup_with_your_email.help_html: 如果你没有要求一封新电邮或怀疑有错, 请访问 %{app_name_html}的 %{help_link_html} 或者 %{contact_link_html}。 diff --git a/lib/ab_test.rb b/lib/ab_test.rb index e92cf24b1a1..1d7be784b49 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -35,7 +35,7 @@ def initialize( # @param [ActionDispatch::Request] request # @param [String,nil] service_provider Issuer string for the service provider associated with # the current session. - # @params [Hash] session + # @param [Hash] session # @param [User] user # @param [Hash] user_session def bucket(request:, service_provider:, session:, user:, user_session:) diff --git a/lib/identity_config.rb b/lib/identity_config.rb index bf04825a906..2b721e2c7a7 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -35,6 +35,7 @@ def self.store config.add(:aamva_private_key, type: :string) config.add(:aamva_public_key, type: :string) config.add(:aamva_send_id_type, type: :boolean) + config.add(:aamva_send_middle_name, type: :boolean) config.add(:aamva_supported_jurisdictions, type: :json) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) @@ -85,6 +86,7 @@ def self.store config.add(:country_phone_number_overrides, type: :json) config.add(:dashboard_api_token, type: :string) config.add(:dashboard_url, type: :string) + config.add(:data_warehouse_enabled, type: :boolean) config.add(:database_advisory_locks_enabled, type: :boolean) config.add(:database_host, type: :string) config.add(:database_name, type: :string) @@ -120,6 +122,7 @@ def self.store config.add(:doc_auth_max_attempts, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) + config.add(:doc_auth_read_additional_pii_attributes_enabled, type: :boolean) config.add(:doc_auth_selfie_desktop_test_mode, type: :boolean) config.add(:doc_auth_socure_wait_polling_refresh_max_seconds, type: :integer) config.add(:doc_auth_socure_wait_polling_timeout_minutes, type: :integer) @@ -420,6 +423,7 @@ def self.store config.add(:socure_docv_webhook_secret_key, type: :string) config.add(:socure_idplus_api_key, type: :string) config.add(:socure_idplus_base_url, type: :string) + config.add(:socure_idplus_shadow_mode_percent, type: :integer) config.add(:socure_idplus_timeout_in_seconds, type: :integer) config.add(:socure_reason_code_api_key, type: :string) config.add(:socure_reason_code_base_url, type: :string) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 77510830474..0f2d44f86ca 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -100,17 +100,22 @@ module Vendors address2: nil, city: 'GREAT FALLS', dob: '1938-10-06', + eye_color: nil, first_name: 'FAKEY', + height: 72, + issuing_country_code: 'US', last_name: 'MCFAKERSON', middle_name: nil, + name_suffix: 'JR', state: 'MT', state_id_expiration: '2099-12-31', state_id_issued: '2019-12-31', state_id_jurisdiction: MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, state_id_number: '1111111111111', state_id_type: 'drivers_license', + sex: 'male', + weight: nil, zipcode: '59010-1234', - issuing_country_code: 'US', }.freeze MOCK_IPP_APPLICANT = { diff --git a/scripts/create-deploy-pr b/scripts/create-deploy-pr index 310d4103beb..4557e1b6040 100755 --- a/scripts/create-deploy-pr +++ b/scripts/create-deploy-pr @@ -51,7 +51,7 @@ function get_next_rc { LAST_RC="$1"; shift MAJOR=$(echo "$LAST_RC" | sed -E 's/\.[0-9]+//') MINOR=$(echo "$LAST_RC" | sed -E 's/[0-9]+(\.|$)//') - + if [ "$PATCH" == "1" ]; then # Doing a patch, so increment minor version by 1 if [ -z "$MINOR" ]; then @@ -77,6 +77,7 @@ function get_staging_sha { } check_gh_configuration +git fetch $GIT_REMOTE RC_BRANCH=stages/rc-$(date +'%Y-%m-%d') if git rev-parse "$GIT_REMOTE/$RC_BRANCH" > /dev/null 2>&1; then diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index e89511a3f05..8c14e6f0924 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -261,4 +261,44 @@ end end end + + describe 'SOCURE_IDV_SHADOW_MODE' do + let(:user) { create(:user) } + + subject(:bucket) do + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session: nil, + ) + end + + before do + allow(IdentityConfig.store).to receive( + :socure_idplus_shadow_mode_percent, + ).and_return(0) + reload_ab_tests + end + + context 'when the A/B test is disabled' do + it 'does not return a bucket' do + expect(bucket).to be_nil + end + end + + context 'when the A/B test is enabled' do + before do + allow(IdentityConfig.store).to receive( + :socure_idplus_shadow_mode_percent, + ).and_return(100) + reload_ab_tests + end + + it 'returns a bucket' do + expect(bucket).to eq :shadow_mode_enabled + end + end + end end diff --git a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb index 4be4fb6fd52..0b66e62c194 100644 --- a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb +++ b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb @@ -28,6 +28,7 @@ expect(assigns(:identity)).to be_kind_of(ServiceProviderIdentity) expect(assigns(:select_email_form)).to be_kind_of(SelectEmailForm) + expect(assigns(:can_add_email)).to eq(true) end context 'with an identity parameter not associated with the user' do @@ -59,6 +60,18 @@ expect(response).to be_not_found end end + + context 'when users has max number of emails' do + before do + allow(user).to receive(:email_address_count).and_return(2) + allow(IdentityConfig.store).to receive(:max_emails_per_account).and_return(2) + end + + it 'can add email variable set to false' do + response + expect(assigns(:can_add_email)).to eq(false) + end + end end describe '#update' do diff --git a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb index 7c52970f208..702fadf58f9 100644 --- a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb +++ b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb @@ -87,7 +87,6 @@ def force_gpo context 'via current idv session' do before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) create(:in_person_enrollment, :establishing, user: user) end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index 7c8554f6f6d..5bbaab77b78 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -414,7 +414,6 @@ def show stub_request_enroll subject.idv_session.applicant = Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID_WITH_PHONE - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 7d83a3b7989..26975325175 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -444,10 +444,15 @@ first_name: first_name, last_name: last_name, middle_name: nil, + name_suffix: nil, address1: address1, state: state, state_id_type: state_id_type, dob: dob, + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_jurisdiction: jurisdiction, state_id_number: state_id_number, zipcode: zipcode, diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index 9ba5c554b0b..489df56f68c 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -374,11 +374,20 @@ end it 'updates proofing component vendor' do - expect(user.proofing_component&.document_check).to be_nil + proofing_components = Idv::ProofingComponents.new( + idv_session: controller.idv_session, + session: controller.session, + user_session: controller.user_session, + user:, + ) + + expect(proofing_components.document_check).to be_nil response - expect(user.proofing_component.document_check).to eq Idp::Constants::Vendors::USPS + user.reload + + expect(proofing_components.document_check).to eq Idp::Constants::Vendors::USPS end end @@ -404,11 +413,20 @@ end it 'updates proofing component vendor' do - expect(user.proofing_component&.document_check).to be_nil + proofing_components = Idv::ProofingComponents.new( + idv_session: controller.idv_session, + session: controller.session, + user_session: controller.user_session, + user:, + ) + + expect(proofing_components.document_check).to be_nil response - expect(user.proofing_component.document_check).to eq Idp::Constants::Vendors::USPS + user.reload + + expect(proofing_components.document_check).to eq Idp::Constants::Vendors::USPS end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index fea657a0e01..9a49ea0c1cd 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -168,9 +168,14 @@ expect(response).to redirect_to(idv_ssn_url) - pc = ProofingComponent.find_by(user_id: user.id) - expect(pc.document_check).to eq('mock') - expect(pc.document_type).to eq('state_id') + proofing_components = Idv::ProofingComponents.new( + idv_session: subject.idv_session, + session: subject.session, + user_session: subject.user_session, + user:, + ) + expect(proofing_components.document_check).to eq('mock') + expect(proofing_components.document_type).to eq('state_id') end context 'redo document capture' do diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index 33946f4bbe1..8238bc1b0f6 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -13,6 +13,11 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) # These keys are present in our applicant fixture but # are not actually supported in Pii::Attributes keys_to_ignore = %i[ + name_suffix + sex + height + weight + eye_color state_id_expiration state_id_issued state_id_number @@ -497,7 +502,6 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) end before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb index 73adfe3dbb4..4966281dccc 100644 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -47,6 +47,7 @@ expect(assigns(:user_emails)).to all be_kind_of(EmailAddress) expect(assigns(:last_sign_in_email_address)).to be_kind_of(String) expect(assigns(:select_email_form)).to be_kind_of(SelectEmailForm) + expect(assigns(:can_add_email)).to eq(true) end context 'with selected email to share feature disabled' do @@ -69,6 +70,18 @@ expect(response).to redirect_to(sign_up_completed_path) end end + + context 'when users has max number of emails' do + before do + allow(user).to receive(:email_address_count).and_return(2) + allow(IdentityConfig.store).to receive(:max_emails_per_account).and_return(2) + end + + it 'can add email variable set to false' do + response + expect(assigns(:can_add_email)).to eq(false) + end + end end describe '#create' do diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index 66eead40d4c..c1483a146d4 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -80,7 +80,7 @@ end trait :facial_match_proof do - idv_level { :in_person } + idv_level { :unsupervised_with_selfie } initiating_service_provider_issuer { 'urn:gov:gsa:openidconnect:inactive:sp:test' } end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index dbe44ada457..b224ab7321e 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -116,6 +116,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, state: 'MT', state_id_jurisdiction: 'ND', @@ -158,6 +159,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: 'ND', state: 'MT', state_id_jurisdiction: 'ND', diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index ca3e1702bb9..be8b28388fc 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -66,7 +66,9 @@ context 'successfully processes image on last attempt' do before do + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) DocAuth::Mock::DocAuthMockClient.reset! + allow(Analytics).to receive(:new).and_return(fake_analytics) end it 'proceeds to the next page with valid info' do @@ -76,13 +78,15 @@ socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - visit idv_socure_document_capture_update_path expect(page).to have_current_path(idv_ssn_url) visit idv_socure_document_capture_path expect(page).to have_current_path(idv_session_errors_rate_limited_path) + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + ) end end end @@ -119,6 +123,11 @@ end context 'standard mobile flow' do + before do + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow(Analytics).to receive(:new).and_return(fake_analytics) + end + it 'proceeds to the next page with valid info' do perform_in_browser(:mobile) do visit_idp_from_oidc_sp_with_ial2 @@ -135,6 +144,9 @@ expect(page).to have_current_path(idv_ssn_url) expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + ) fill_out_ssn_form_ok click_idv_continue diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index dc2c5123915..8c516ce67bd 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -33,7 +33,7 @@ validate_verify_info_page complete_verify_step - validate_verify_info_submit(user) + validate_verify_info_submit validate_phone_page try_to_skip_ahead_from_phone @@ -265,12 +265,8 @@ def validate_verify_info_page expect(page).to have_text(DocAuthHelper::GOOD_SSN) end - def validate_verify_info_submit(user) + def validate_verify_info_submit expect(page).to have_content(t('doc_auth.forms.doc_success')) - expect(user.proofing_component.resolution_check).to eq(Idp::Constants::Vendors::LEXIS_NEXIS) - expect(user.proofing_component.source_check).to satisfy do |v| - Idp::Constants::Vendors::SOURCE_CHECK.include?(v) - end end def validate_phone_page @@ -333,6 +329,15 @@ def validate_enter_password_submit(user) profile = user.profiles.first expect(profile.active?).to eq true + expect(profile.proofing_components).to eql( + 'source_check' => 'StateIdMock', + 'threatmetrix' => true, + 'address_check' => 'lexis_nexis_address', + 'document_type' => 'state_id', + 'document_check' => 'mock', + 'resolution_check' => 'lexis_nexis', + 'threatmetrix_review_status' => 'pass', + ) expect(GpoConfirmation.count).to eq(0) end diff --git a/spec/fixtures/proofing/aamva/responses/verification_response.xml b/spec/fixtures/proofing/aamva/responses/verification_response.xml index a55dc4e890c..9c32b010b25 100644 --- a/spec/fixtures/proofing/aamva/responses/verification_response.xml +++ b/spec/fixtures/proofing/aamva/responses/verification_response.xml @@ -11,16 +11,24 @@ true true true + true + true + true + true true true true true true true + true + true + true + true true true true true true true - \ No newline at end of file + diff --git a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml index 6fdc71ded19..63e537697e1 100644 --- a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml +++ b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml @@ -20,8 +20,14 @@ true true true + true + true + true + true true true + true + true true true true @@ -30,4 +36,4 @@ - \ No newline at end of file + diff --git a/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb b/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb index cab54a33c7c..7d442e98706 100644 --- a/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb +++ b/spec/forms/event_disavowal/password_reset_from_disavowal_form_spec.rb @@ -26,26 +26,4 @@ expect(user.reload.valid_password?(new_password)).to eq(false) end end - - context 'user has an active profile' do - let(:user) { create(:user, :proofed) } - - it 'destroys the proofing component' do - ProofingComponent.create(user_id: user.id, document_check: 'mock') - - subject.submit(password: new_password) - - expect(user.reload.proofing_component).to be_nil - end - end - - context 'user does not have an active profile' do - it 'does not destroy the proofing component' do - ProofingComponent.create(user_id: user.id, document_check: 'mock') - - subject.submit(password: new_password) - - expect(user.reload.proofing_component).to_not be_nil - end - end end diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb index 4aad640877a..6c50506aaa2 100644 --- a/spec/forms/gpo_verify_form_spec.rb +++ b/spec/forms/gpo_verify_form_spec.rb @@ -156,10 +156,6 @@ ) end - let(:proofing_components) do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) - end - before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end @@ -194,10 +190,6 @@ ) end - let(:proofing_components) do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) - end - before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 220d6842536..960aa40598c 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -74,6 +74,7 @@ class BaseTask { key: 'time.formats.event_timestamp', locales: %i[zh] }, { key: 'time.formats.full_date', locales: %i[es] }, # format is the same in Spanish and English { key: 'time.formats.sms_date' }, # for us date format + { key: 'user_mailer.reset_password_instructions.in_person_warning_description_html', locales: %i[es fr zh] }, # Temporary until spanish, french, and chinese translations come in. { key: 'webauthn_platform_recommended.cta' }, # English-only A/B test { key: 'webauthn_platform_recommended.description_private_html' }, # English-only A/B test { key: 'webauthn_platform_recommended.description_secure_account' }, # English-only A/B test diff --git a/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb index 45e022abb9f..980f93ce836 100644 --- a/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb +++ b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb @@ -6,6 +6,7 @@ let(:expected_bucket) { 'login-gov-analytics-export-test-1234-us-west-2' } let(:test_on_tables) { ['users'] } let(:s3_data_warehouse_bucket_prefix) { 'login-gov-analytics-export' } + let(:data_warehouse_enabled) { true } let(:expected_json) do { @@ -30,7 +31,8 @@ allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') allow(IdentityConfig.store).to receive(:s3_data_warehouse_bucket_prefix). and_return(s3_data_warehouse_bucket_prefix) - + allow(IdentityConfig.store).to receive(:data_warehouse_enabled). + and_return(data_warehouse_enabled) Aws.config[:s3] = { stub_responses: { put_object: {}, @@ -44,6 +46,17 @@ add_data_to_tables end + context 'when data_warehouse_enabled is false' do + let(:data_warehouse_enabled) { false } + + it 'does not perform the job' do + allow(IdentityConfig.store).to receive(:data_warehouse_enabled). + and_return(data_warehouse_enabled) + expect(job).not_to receive(:fetch_table_max_ids_and_counts) + expect(job).not_to receive(:upload_file_to_s3_bucket) + end + end + context 'when database tables contain data' do it 'generates correct JSON from database tables' do json_data = job.fetch_table_max_ids_and_counts(timestamp) diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index 0ce568616d6..1af200134b2 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -121,9 +121,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], ) @@ -138,10 +145,6 @@ expect(result_context_stages_threatmetrix[:response_body]).to eq( JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json, symbolize_names: true), ) - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(true) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end @@ -213,9 +216,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], ) end @@ -340,10 +350,6 @@ expect(result_context_stages_threatmetrix[:client]).to eq('tmx_disabled') expect(@threatmetrix_stub).to_not have_been_requested - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(false) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end @@ -446,9 +452,16 @@ state_id_expiration state_id_issued state_id_number - state_id_type dob + state_id_type + dob last_name first_name + middle_name + name_suffix + height + sex + weight + eye_color ], ) @@ -463,10 +476,6 @@ expect(result_context_stages_threatmetrix[:response_body]).to eq( JSON.parse(LexisNexisFixtures.ddp_success_redacted_response_json, symbolize_names: true), ) - - proofing_component = user.proofing_component - expect(proofing_component.threatmetrix).to equal(true) - expect(proofing_component.threatmetrix_review_status).to eq('pass') end end @@ -538,7 +547,7 @@ context 'socure shadow mode' do context 'turned on' do before do - allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled).and_return(true) + allow(instance).to receive(:use_shadow_mode?).and_return(true) end it 'schedules a SocureShadowModeProofingJob' do @@ -557,6 +566,7 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: 'JR', address1: '1 FAKE RD', identity_doc_address1: '1 FAKE RD', identity_doc_address2: nil, @@ -570,6 +580,10 @@ state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: 'male', + height: 72, + weight: nil, + eye_color: nil, ssn: '900-66-1234', state_id_jurisdiction: 'ND', state_id_expiration: '2099-12-31', diff --git a/spec/jobs/socure_docv_results_job_spec.rb b/spec/jobs/socure_docv_results_job_spec.rb index 5ded063eaa9..92bfa049db3 100644 --- a/spec/jobs/socure_docv_results_job_spec.rb +++ b/spec/jobs/socure_docv_results_job_spec.rb @@ -5,6 +5,7 @@ RSpec.describe SocureDocvResultsJob do let(:job) { described_class.new } let(:user) { create(:user) } + let(:fake_analytics) { FakeAnalytics.new } let(:document_capture_session) do DocumentCaptureSession.create(user:).tap do |dcs| dcs.socure_docv_transaction_token = '1234' @@ -15,17 +16,19 @@ let(:decision_value) { 'accept' } let(:expiration_date) { "#{1.year.from_now.year}-01-01" } - let(:analytics) { FakeAnalytics.new } - before do allow(IdentityConfig.store).to receive(:socure_idplus_base_url). and_return(socure_idplus_base_url) + allow(Analytics).to receive(:new).and_return(fake_analytics) end describe '#perform' do subject(:perform) do job.perform(document_capture_session_uuid: document_capture_session_uuid) end + subject(:perform_now) do + job.perform(document_capture_session_uuid: document_capture_session_uuid, async: false) + end let(:socure_response_body) do # ID+ v3.0 API Predictive Document Verification response @@ -69,6 +72,24 @@ } end + let(:expected_socure_log) do + { + success: true, + issue_year: 2020, + vendor: 'Socure', + submit_attempts: 0, + remaining_submit_attempts: 4, + state: 'NY', + zip_code: '10001', + doc_auth_success: true, + document_type: { + type: 'Drivers License', + country: 'USA', + state: 'NY', + }, + } + end + before do stub_request(:post, 'https://example.com/api/3.0/EmailAuthScore'). to_return( @@ -91,6 +112,26 @@ expect(document_capture_session_result.selfie_status).to eq(:not_processed) end + it 'expect fake analytics to have logged idv_socure_verification_data_requested' do + perform + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + hash_including( + expected_socure_log.merge({ async: true }), + ), + ) + end + + it 'expect log with perform_now to have async eq false' do + perform_now + expect(fake_analytics).to have_logged_event( + :idv_socure_verification_data_requested, + hash_including( + expected_socure_log.merge({ async: false }), + ), + ) + end + context 'when the document capture session does not exist' do let(:document_capture_session_uuid) { '1234' } diff --git a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb index 9416a03e907..92e93072894 100644 --- a/spec/jobs/socure_shadow_mode_proofing_job_spec.rb +++ b/spec/jobs/socure_shadow_mode_proofing_job_spec.rb @@ -106,6 +106,7 @@ }, }, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, same_address_as_id: nil, state: 'MT', @@ -257,6 +258,7 @@ threatmetrix_review_status: 'pass', timed_out: false, biographical_info: { + birth_year: 1938, identity_doc_address_state: nil, same_address_as_id: nil, state: 'MT', diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 5aa21140cc6..4bf1af6242b 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -26,6 +26,14 @@ def reset_password_instructions_with_pending_gpo_letter ) end + def reset_password_instructions_with_pending_in_person_warning + UserMailer.with( + user: user_with_pending_in_person_profile, email_address: email_address_record, + ).reset_password_instructions( + token: SecureRandom.hex, request_id: SecureRandom.hex, + ) + end + def password_changed UserMailer.with(user: user, email_address: email_address_record). password_changed(disavowal_token: SecureRandom.hex) @@ -313,6 +321,19 @@ def user_with_pending_gpo_letter raw_user end + def user_with_pending_in_person_profile + raw_user = user + in_person_pending_profile = unsaveable( + Profile.new( + user: raw_user, + active: false, + in_person_verification_pending_at: Time.zone.now, + ), + ) + raw_user.send(:instance_variable_set, :@pending_profile, in_person_pending_profile) + raw_user + end + def email_address 'email@example.com' end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 15a4ffb1fda..8155d26713a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -113,6 +113,152 @@ end end + describe '#reset_password_instructions' do + let(:token) { SecureRandom.hex } + let(:request_id) { SecureRandom.uuid } + let(:mail) do + UserMailer.with( + user: user, + email_address: email_address, + ).reset_password_instructions(token:, request_id:) + end + let(:locale) { 'es' } + + before do + I18n.locale = locale + end + + it_behaves_like 'a system email' + it_behaves_like 'an email that respects user email locale preference' + + context 'when the user has gpo verfication pending' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'renders the gpo warning alert' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'does not render the in person warning banner' do + expect(mail.html_part.body).not_to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + + context 'when the user has in person verfication pending' do + let(:user) { create(:user, :with_pending_in_person_enrollment) } + + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'renders the in person warning banner' do + expect(mail.html_part.body).to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'does not render the gpo warning alert' do + expect(mail.html_part.body).not_to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + + context 'when the user does not have any verification pending' do + it 'sends to the current email address' do + expect(mail.to).to eq [email_address.email] + end + + it 'renders the subject' do + expect(mail.subject).to eq t('user_mailer.reset_password_instructions.subject') + end + + it 'does not render the gpo warning alert' do + expect(mail.html_part.body).not_to have_content( + t('user_mailer.reset_password_instructions.gpo_letter_description'), + ) + end + + it 'does not render the in person warning banner' do + expect(mail.html_part.body).not_to have_content( + strip_tags( + t('user_mailer.reset_password_instructions.in_person_warning_description_html'), + ), + ) + end + + it 'renders the reset password instructions' do + expect(mail.html_part.body).to have_content( + t('user_mailer.reset_password_instructions.header'), + ) + end + + it 'renders the reset password button' do + expect(mail.html_part.body).to have_link( + t('user_mailer.reset_password_instructions.link_text'), + href: edit_user_password_url( + reset_password_token: token, + locale: locale, + request_id: request_id, + ), + ) + end + end + end + describe '#password_changed' do let(:mail) do UserMailer.with( diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index b1056460a49..0219451dd6e 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -8,11 +8,16 @@ first_name: 'Testy', last_name: 'Testy', middle_name: nil, + name_suffix: nil, address1: '123 ABC AVE', address2: nil, city: 'ANYTOWN', state: 'MD', dob: '1986-07-01', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 94c77ed8f6c..288f591a368 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,7 +10,6 @@ it { is_expected.to have_one(:account_reset_request) } it { is_expected.to have_many(:phone_configurations) } it { is_expected.to have_many(:webauthn_configurations) } - it { is_expected.to have_one(:proofing_component) } it { is_expected.to have_many(:in_person_enrollments).dependent(:destroy) } it { is_expected.to have_one(:pending_in_person_enrollment). @@ -333,9 +332,6 @@ describe '#has_in_person_enrollment?' do it 'returns the establishing IPP enrollment that has an address' do - ProofingComponent.find_or_create_by(user: user). - update!(document_check: Idp::Constants::Vendors::USPS) - expect(user.has_in_person_enrollment?).to eq(true) end end 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 7847a830725..2d3584ce73d 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 @@ -127,11 +127,16 @@ first_name: 'DAVID', last_name: 'SAMPLE', middle_name: 'LICENSE', + name_suffix: nil, address1: '123 ABC AVE', address2: 'APT 3E', city: 'ANYTOWN', state: 'MD', dob: '1986-07-01', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', @@ -244,6 +249,22 @@ expect(response.success?).to eq(false) end end + + context 'when doc_auth_read_additional_pii_attributes_enabled is enabled' do + let(:success_response_body) { LexisNexisFixtures.true_id_response_success } + + it 'reads the additional PII attributes' do + allow(IdentityConfig.store).to receive(:doc_auth_read_additional_pii_attributes_enabled). + and_return(true) + + pii_from_doc = response.pii_from_doc + + expect(pii_from_doc.first_name).to eq('LICENSE') + expect(pii_from_doc.name_suffix).to eq('JR') + expect(pii_from_doc.sex).to eq('male') + expect(pii_from_doc.height).to eq(68) + end + end end context 'when there is no address line 2' do @@ -326,11 +347,16 @@ def get_decision_product(resp) first_name: 'DAVID', last_name: 'SAMPLE', middle_name: 'LICENSE', + name_suffix: nil, address1: '123 ABC AVE', address2: nil, city: 'ANYTOWN', state: 'MD', dob: '1986-10-13', + sex: nil, + height: nil, + weight: nil, + eye_color: nil, state_id_expiration: '2099-10-15', state_id_issued: '2016-10-15', state_id_jurisdiction: 'MD', 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 8206bbe528f..ddfade0d576 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 @@ -28,12 +28,17 @@ first_name: 'FAKEY', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: 'male', + height: 72, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -50,12 +55,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: 'SR' address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 1938-10-06 + sex: 'female' + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -84,12 +92,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: 'SR', address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: 'female', + height: 66, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -127,12 +140,17 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: 'male', + height: 72, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index 7988ac33622..f79c503e1fb 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -34,12 +34,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 1938-10-06 + sex: female + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -58,12 +61,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: 'female', + height: 66, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -277,13 +285,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: '11364' dob: 10/06/1938 - phone: +1 314-555-1212 + sex: female + height: 66 state_id_number: '123456789' state_id_type: drivers_license state_id_jurisdiction: 'NY' @@ -302,6 +312,7 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', @@ -310,6 +321,10 @@ state_id_number: '123456789', zipcode: '11364', dob: '1938-10-06', + sex: 'female', + height: 66, + weight: nil, + eye_color: nil, state_id_type: 'drivers_license', state_id_expiration: '2089-12-31', state_id_issued: '2009-12-31', @@ -356,12 +371,17 @@ first_name: 'Susan', middle_name: nil, last_name: 'MCFAKERSON', + name_suffix: 'JR', address1: '1 FAKE RD', address2: nil, city: 'GREAT FALLS', state: 'MT', zipcode: '59010-1234', dob: '1938-10-06', + sex: 'male', + height: 72, + weight: nil, + eye_color: nil, state_id_number: '1111111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', @@ -480,12 +500,15 @@ first_name: Susan last_name: Smith middle_name: Q + name_suffix: address1: 1 Microsoft Way address2: Apt 3 city: Bayside state: NY zipcode: 11364 dob: 1938-10-06 + sex: female + height: 66 state_id_number: '111111111' state_id_jurisdiction: ND state_id_type: drivers_license @@ -504,12 +527,17 @@ first_name: 'Susan', middle_name: 'Q', last_name: 'Smith', + name_suffix: nil, address1: '1 Microsoft Way', address2: 'Apt 3', city: 'Bayside', state: 'NY', zipcode: '11364', dob: '1938-10-06', + sex: 'female', + height: 66, + weight: nil, + eye_color: nil, state_id_number: '111111111', state_id_jurisdiction: 'ND', state_id_type: 'drivers_license', diff --git a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb index 2fe380431d8..ae7a67a63c7 100644 --- a/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb +++ b/spec/services/doc_auth/socure/requests/docv_result_request_spec.rb @@ -3,6 +3,7 @@ RSpec.describe DocAuth::Socure::Requests::DocvResultRequest do let(:document_capture_session_uuid) { 'fake uuid' } let(:biometric_comparison_required) { false } + let(:fake_analytics) { FakeAnalytics.new } subject(:docv_result_request) do described_class.new( diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index df023971d8d..09dd6ec941a 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -193,7 +193,6 @@ let(:profile) { subject.profile } before do - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) subject.user_phone_confirmation = true subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.with_indifferent_access @@ -272,7 +271,6 @@ before do allow(UspsInPersonProofing::EnrollmentHelper).to receive(:schedule_in_person_enrollment) - ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) subject.user_phone_confirmation = true subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.with_indifferent_access diff --git a/spec/services/proofing/aamva/applicant_spec.rb b/spec/services/proofing/aamva/applicant_spec.rb index b2172ce9bd5..cb53ffeed65 100644 --- a/spec/services/proofing/aamva/applicant_spec.rb +++ b/spec/services/proofing/aamva/applicant_spec.rb @@ -64,4 +64,12 @@ expect(aamva_applicant[:dob]).to eq('') end + + it 'should format the height' do + proofer_applicant[:height] = 73 + aamva_applicant = Proofing::Aamva::Applicant.from_proofer_applicant(proofer_applicant) + + # This is intended to describe 6'1" + expect(aamva_applicant[:height]).to eq('61') + end end diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index e14c2f44aa0..33bbdfb169f 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -338,6 +338,108 @@ def self.test_not_successful test_not_in_verified_attributes end end + + describe '#middle_name' do + let(:attribute) { :middle_name } + let(:match_indicator_name) { 'PersonMiddleNameExactMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#name_suffix' do + let(:attribute) { :name_suffix } + let(:match_indicator_name) { 'PersonNameSuffixMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#height' do + let(:attribute) { :height } + let(:match_indicator_name) { 'PersonHeightMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#sex' do + let(:attribute) { :sex } + let(:match_indicator_name) { 'PersonSexCodeMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#weight' do + let(:attribute) { :weight } + let(:match_indicator_name) { 'PersonWeightMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end + + describe '#eye_color' do + let(:attribute) { :eye_color } + let(:match_indicator_name) { 'PersonEyeColorMatchIndicator' } + + when_unverified do + test_still_successful + test_in_requested_attributes + test_not_in_verified_attributes + end + + when_missing do + test_still_successful + test_not_in_requested_attributes + test_not_in_verified_attributes + end + end end context 'when verification is successful' do @@ -361,7 +463,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -377,7 +485,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end @@ -410,7 +524,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -426,7 +546,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end @@ -458,7 +584,13 @@ def self.test_not_successful state_id_type last_name first_name + middle_name + name_suffix address + height + sex + weight + eye_color ].to_set, ) end @@ -473,7 +605,13 @@ def self.test_not_successful state_id_type: 1, last_name: 1, first_name: 1, + middle_name: 1, + name_suffix: 1, address: 1, + height: 1, + sex: 1, + weight: 1, + eye_color: 1, }, ) end diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 68c2a874d83..187b766fd01 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -89,6 +89,87 @@ ) end + it 'includes height if it is present' do + applicant.height = '63' + expect(subject.body).to include( + '63', + ) + end + + it 'includes weight if it is present' do + applicant.weight = 190 + expect(subject.body).to include( + '190', + ) + end + + it 'includes eye_color if it is present' do + applicant.eye_color = 'blu' + expect(subject.body).to include( + 'blu', + ) + end + + it 'includes name_suffix if it is present' do + applicant.name_suffix = 'JR' + expect(subject.body).to include( + 'JR', + ) + end + + context '#sex' do + context 'when the sex is male' do + it 'sends a sex code value of 1' do + applicant.sex = 'male' + expect(subject.body).to include( + '1', + ) + end + end + + context 'when the sex is female' do + it 'sends a sex code value of 2' do + applicant.sex = 'female' + expect(subject.body).to include( + '2', + ) + end + end + + context 'when the sex is blank' do + it 'does not send a sex code value' do + applicant.sex = nil + expect(subject.body).to_not include('') + end + end + end + + context '#middle_name' do + context 'when the feature flag is off' do + before do + allow(IdentityConfig.store).to receive(:aamva_send_middle_name).and_return(false) + end + + it 'does not add a PersonMiddleName node' do + applicant.middle_name = 'test_name' + expect(subject.body).to_not include('') + end + end + + context 'when the feature flag is on' do + before do + allow(IdentityConfig.store).to receive(:aamva_send_middle_name).and_return(true) + end + + it 'does add a PersonMiddleName node' do + applicant.middle_name = 'test_name' + expect(subject.body).to include( + 'test_name', + ) + end + end + end + context '#state_id_type' do context 'when the feature flag is off' do before do diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb index 3c91ffb880a..322abe22f91 100644 --- a/spec/services/proofing/aamva/response/verification_response_spec.rb +++ b/spec/services/proofing/aamva/response/verification_response_spec.rb @@ -18,8 +18,14 @@ state_id_number: true, state_id_type: true, dob: true, + height: true, + sex: true, + weight: true, + eye_color: true, last_name: true, first_name: true, + middle_name: true, + name_suffix: true, address1: true, address2: true, city: true, diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb index 25fefd7702e..14a765a2445 100644 --- a/spec/services/proofing/aamva/verification_client_spec.rb +++ b/spec/services/proofing/aamva/verification_client_spec.rb @@ -65,8 +65,14 @@ address2: true, city: true, dob: true, + height: true, + sex: true, + weight: true, + eye_color: true, first_name: true, last_name: true, + middle_name: true, + name_suffix: true, state: true, state_id_expiration: true, state_id_issued: true, @@ -96,8 +102,14 @@ address2: true, city: true, dob: false, + height: true, + sex: true, + weight: true, + eye_color: true, first_name: true, last_name: true, + middle_name: true, + name_suffix: true, state: true, state_id_expiration: true, state_id_issued: true, diff --git a/spec/services/proofing/resolution/result_adjudicator_spec.rb b/spec/services/proofing/resolution/result_adjudicator_spec.rb index c63a10e4c0a..6e8aba8bbfb 100644 --- a/spec/services/proofing/resolution/result_adjudicator_spec.rb +++ b/spec/services/proofing/resolution/result_adjudicator_spec.rb @@ -102,6 +102,7 @@ result = subject.adjudicated_result expect(result.extra[:biographical_info]).to eq( + birth_year: 1938, state: 'MT', identity_doc_address_state: nil, state_id_jurisdiction: 'ND', @@ -118,6 +119,7 @@ result = subject.adjudicated_result expect(result.extra[:biographical_info]).to eq( + birth_year: 1938, state: 'MT', identity_doc_address_state: 'MT', state_id_jurisdiction: 'ND', diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb index 454ddb9144b..a0e0fe82a02 100644 --- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb @@ -20,6 +20,7 @@ allow(view).to receive(:current_user).and_return(user) @identity = identity @select_email_form = SelectEmailForm.new(user:, identity:) + @can_add_email = true end it 'renders introduction text' do @@ -35,4 +36,24 @@ expect(inputs).to be_logically_grouped(t('titles.select_email')) expect(rendered).to have_content(identity.display_name) end + + it 'renders a button to allow users to add email' do + expect(rendered).to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + + context 'if user has reached max number of emails' do + before do + @can_add_email = false + end + + it 'does not render add email button' do + expect(rendered).not_to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + end end diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb index 30d16c34b92..86352280d0e 100644 --- a/spec/views/sign_up/select_email/show.html.erb_spec.rb +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -12,6 +12,7 @@ @user_emails = user.confirmed_email_addresses @select_email_form = SelectEmailForm.new(user:) @sp_name = 'Test Service Provider' + @can_add_email = true end it 'renders introduction text' do @@ -24,4 +25,24 @@ expect(rendered).to include('michael.motorist@email.com') expect(rendered).to include('michael.motorist2@email.com') end + + it 'renders a button to allow users to add email' do + expect(rendered).to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + + context 'if user has reached max number of emails' do + before do + @can_add_email = false + end + + it 'does not render add email button' do + expect(rendered).not_to have_link( + t('account.index.email_add'), + href: add_email_path(in_select_email_flow: true), + ) + end + end end