From 95415945bdff755d41fa3ba305e60d17b5fa5d4e Mon Sep 17 00:00:00 2001 From: Stephen Shelton Date: Thu, 15 Aug 2024 11:44:26 -0400 Subject: [PATCH 01/22] Adding labels to kubernetes resources for easier tracing (#11081) * changelog: Internal, CI, adding labels to kubernetes resources for easier tracing --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b77bf0700e0..afe4b702609 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -430,6 +430,9 @@ trigger_devops: - kubectl config get-contexts - export CONTEXT=$(kubectl config get-contexts | grep reviewapp | awk '{print $1}' | head -1) - kubectl config use-context "$CONTEXT" + - export SANITIZED_BRANCH_NAME=$(echo "$CI_COMMIT_REF_NAME" | tr '/' '-' | tr -c '[:alnum:]-_' '-' | sed 's/-*$//') + - echo "${CI_COMMIT_REF_NAME}" + - echo "${SANITIZED_BRANCH_NAME}" - |- export IDP_CONFIG=$(cat <- helm upgrade --install --namespace review-apps --debug + --set global.labels.branch="${SANITIZED_BRANCH_NAME}" --set env="reviewapps-$CI_ENVIRONMENT_SLUG" --set idp.image.repository="${ECR_REGISTRY}/identity-idp/review" --set idp.image.tag="${CI_COMMIT_SHA}" From cf000823195acb5f1f7e30fbe8b749bc14bbad9a Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:11:12 -0400 Subject: [PATCH 02/22] Refactor backup code verification to follow conventional form pattern (#11089) changelog: Internal, Code Quality, Refactor backup code verification to follow conventional form pattern --- .../backup_code_verification_controller.rb | 6 +- app/forms/backup_code_verification_form.rb | 14 ++- app/services/backup_code_generator.rb | 5 - ...ackup_code_verification_controller_spec.rb | 4 +- .../backup_code_verification_form_spec.rb | 38 +++--- spec/services/backup_code_generator_spec.rb | 110 +++++++++++------- 6 files changed, 95 insertions(+), 82 deletions(-) diff --git a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb index 9101b2784b6..f2fc8e08eb6 100644 --- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb +++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb @@ -46,10 +46,10 @@ def presenter_for_two_factor_authentication_method ) end - def handle_invalid_backup_code + def handle_invalid_backup_code(result) update_invalid_user - flash.now[:error] = t('two_factor_authentication.invalid_backup_code') + flash.now[:error] = result.first_error_message if current_user.locked_out? handle_second_factor_locked_user(type: 'backup_code') @@ -69,7 +69,7 @@ def handle_result(result) return handle_last_code if all_codes_used? handle_valid_backup_code else - handle_invalid_backup_code + handle_invalid_backup_code(result) end end diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index eb871991c04..57e5d14aa14 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -2,6 +2,9 @@ class BackupCodeVerificationForm include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + validate :validate_and_consume_backup_code! def initialize(user) @user = user @@ -11,20 +14,27 @@ def initialize(user) def submit(params) @backup_code = params[:backup_code] FormResponse.new( - success: valid_backup_code?, + success: valid?, + errors:, extra: extra_analytics_attributes, + serialize_error_details_only: true, ) end attr_reader :user, :backup_code + def validate_and_consume_backup_code! + return if valid_backup_code? + errors.add(:backup_code, :invalid, message: t('two_factor_authentication.invalid_backup_code')) + end + def valid_backup_code? valid_backup_code_config_created_at.present? end def valid_backup_code_config_created_at return @valid_backup_code_config_created_at if defined?(@valid_backup_code_config_created_at) - @valid_backup_code_config_created_at = BackupCodeGenerator.new(@user). + @valid_backup_code_config_created_at = BackupCodeGenerator.new(user). if_valid_consume_code_return_config_created_at(backup_code) end diff --git a/app/services/backup_code_generator.rb b/app/services/backup_code_generator.rb index e7b0d0ca615..9753108c251 100644 --- a/app/services/backup_code_generator.rb +++ b/app/services/backup_code_generator.rb @@ -23,11 +23,6 @@ def delete_and_regenerate(salt: SecureRandom.hex(32)) codes end - # @return [Boolean] - def verify(plaintext_code) - if_valid_consume_code_return_config_created_at(plaintext_code).present? - end - # @return [BackupCodeConfiguration, nil] def if_valid_consume_code_return_config_created_at(plaintext_code) return unless plaintext_code.present? diff --git a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb index de8667bcfcd..2b474fdf885 100644 --- a/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/backup_code_verification_controller_spec.rb @@ -37,7 +37,6 @@ expect(@analytics).to have_logged_event( 'Multi-Factor Authentication', success: true, - errors: {}, multi_factor_auth_method: 'backup_code', multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'), enabled_mfa_methods_count: 1, @@ -94,7 +93,6 @@ expect(@analytics).to have_logged_event( 'Multi-Factor Authentication', success: true, - errors: {}, multi_factor_auth_method: 'backup_code', multi_factor_auth_method_created_at: Time.zone.now.strftime('%s%L'), enabled_mfa_methods_count: 1, @@ -173,7 +171,7 @@ expect(@analytics).to have_logged_event( 'Multi-Factor Authentication', success: false, - errors: {}, + error_details: { backup_code: { invalid: true } }, multi_factor_auth_method: 'backup_code', enabled_mfa_methods_count: 1, new_device: true, diff --git a/spec/forms/backup_code_verification_form_spec.rb b/spec/forms/backup_code_verification_form_spec.rb index aa3e8342103..58708037c20 100644 --- a/spec/forms/backup_code_verification_form_spec.rb +++ b/spec/forms/backup_code_verification_form_spec.rb @@ -10,45 +10,35 @@ end describe '#submit' do - let(:params) do - { - backup_code: code, - } - end + let(:params) { { backup_code: code } } context 'with a valid backup code' do let(:code) { backup_codes.first } - let(:expected_response) do - { + + it 'returns success' do + expect(result).to eq( success: true, - errors: {}, multi_factor_auth_method_created_at: backup_code_config.created_at.strftime('%s%L'), - } - end - - it 'returns succcess' do - expect(result).to eq(expected_response) + ) end it 'marks code as used' do - expect { subject }.to change { - backup_code_config.reload.used_at - }.from(nil).to kind_of(Time) + expect { subject }. + to change { backup_code_config.reload.used_at }. + from(nil). + to kind_of(Time) end end context 'with an invalid backup code' do let(:code) { 'invalid' } - let(:expected_response) do - { - success: false, - errors: {}, - multi_factor_auth_method_created_at: nil, - } - end it 'returns failure' do - expect(result).to eq(expected_response) + expect(result).to eq( + success: false, + error_details: { backup_code: { invalid: true } }, + multi_factor_auth_method_created_at: nil, + ) end end end diff --git a/spec/services/backup_code_generator_spec.rb b/spec/services/backup_code_generator_spec.rb index 9d980f5a124..24e6ac14dc2 100644 --- a/spec/services/backup_code_generator_spec.rb +++ b/spec/services/backup_code_generator_spec.rb @@ -5,70 +5,90 @@ subject(:generator) { BackupCodeGenerator.new(user) } - it 'should generate backup codes and be able to verify them' do - codes = generator.delete_and_regenerate - - codes.each do |code| - expect(generator.verify(code)).to eq(true) + describe '#delete_and_regenerate' do + subject(:codes) { generator.delete_and_regenerate } + + it 'generates backup codes' do + expect { codes }. + to change { user.reload.backup_code_configurations.count }. + from(0). + to(BackupCodeGenerator::NUMBER_OF_CODES) end - end - it 'generates 12-letter/digit codes via base32 crockford' do - expect(Base32::Crockford).to receive(:encode). - and_call_original.at_least(BackupCodeGenerator::NUMBER_OF_CODES).times + it 'returns valid 12-character codes via base32 crockford' do + expect(Base32::Crockford).to receive(:encode). + and_call_original.at_least(BackupCodeGenerator::NUMBER_OF_CODES).times - codes = generator.delete_and_regenerate + expect(codes).to be_present + codes.each do |code| + expect(code).to match(/\A[a-z0-9]{12}\Z/i) + expect(generator.if_valid_consume_code_return_config_created_at(code)).not_to eq(nil) + end + end - codes.each do |code| - expect(code).to match(/\A[a-z0-9]{12}\Z/i) + it 'should generate backup codes and be able to verify them' do + codes.each do |code| + expect(generator.if_valid_consume_code_return_config_created_at(code)).not_to eq(nil) + end end - end - it 'should reject invalid codes' do - generator.delete_and_regenerate + it 'creates codes with the same salt for that batch' do + codes - success = generator.verify 'This is a string which will never result from code generation' - expect(success).to eq false - end + salts = user.backup_code_configurations.map(&:code_salt).uniq + expect(salts.size).to eq(1) + expect(salts.first).to_not be_empty - it 'should reject nil codes' do - success = generator.verify(nil) - expect(success).to eq false - end + costs = user.backup_code_configurations.map(&:code_cost).uniq + expect(costs.size).to eq(1) + expect(costs.first).to_not be_empty + end - it 'creates codes with the same salt for that batch' do - generator.delete_and_regenerate + it 'creates different salts for different batches' do + user1 = create(:user) + user2 = create(:user) - salts = user.backup_code_configurations.map(&:code_salt).uniq - expect(salts.size).to eq(1) - expect(salts.first).to_not be_empty + [user1, user2].each { |user| BackupCodeGenerator.new(user).delete_and_regenerate } - costs = user.backup_code_configurations.map(&:code_cost).uniq - expect(costs.size).to eq(1) - expect(costs.first).to_not be_empty - end + user1_salt = user1.backup_code_configurations.map(&:code_salt).uniq.first + user2_salt = user2.backup_code_configurations.map(&:code_salt).uniq.first - it 'creates different salts for different batches' do - user1 = create(:user) - user2 = create(:user) + expect(user1_salt).to_not eq(user2_salt) + end - [user1, user2].each { |user| BackupCodeGenerator.new(user).delete_and_regenerate } + it 'filters out profanity' do + profane = Base32::Crockford.decode('FART') + not_profane = Base32::Crockford.decode('ABCD') - user1_salt = user1.backup_code_configurations.map(&:code_salt).uniq.first - user2_salt = user2.backup_code_configurations.map(&:code_salt).uniq.first + expect(SecureRandom).to receive(:random_number). + and_return(profane, not_profane) - expect(user1_salt).to_not eq(user2_salt) + code = generator.send(:backup_code) + + expect(code).to eq('00000000ABCD') + end end - it 'filters out profanity' do - profane = Base32::Crockford.decode('FART') - not_profane = Base32::Crockford.decode('ABCD') + describe '#if_valid_consume_code_return_config_created_at' do + let(:code) {} + subject(:result) { generator.if_valid_consume_code_return_config_created_at(code) } + + context 'invalid code' do + let(:code) { 'invalid' } + + it { is_expected.to eq(nil) } + end + + context 'nil code' do + let(:code) { 'invalid' } - expect(SecureRandom).to receive(:random_number). - and_return(profane, not_profane) + it { is_expected.to eq(nil) } + end - code = generator.send(:backup_code) + context 'valid code' do + let(:code) { generator.delete_and_regenerate.sample } - expect(code).to eq('00000000ABCD') + it { is_expected.to be_instance_of(Time) } + end end end From 9ffd3a3aacb8a4e18da3026f88ab7cf33dd67f36 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Fri, 16 Aug 2024 17:03:42 +0000 Subject: [PATCH 03/22] Do not write config file by default on boot (#11100) changelog: Internal, Configuration, Do not write config file by default on boot --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index 69e9d89dfea..5b544fd382d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -35,7 +35,7 @@ class Application < Rails::Application Identity::Hostdata.load_config!( app_root: Rails.root, rails_env: Rails.env, - write_copy_to: Rails.root.join('tmp', 'application.yml'), + write_copy_to: nil, &IdentityConfig::BUILDER ) From 3674e15759cd3c11ced138dd0b2ae1ba5e53510d Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:04:34 -0400 Subject: [PATCH 04/22] Use defer for non-critical scripts (#11096) changelog: User-Facing Improvements, Performance, Use defer for non-critical scripts --- app/views/devise/sessions/new.html.erb | 2 +- app/views/layouts/base.html.erb | 2 +- spec/helpers/script_helper_spec.rb | 4 ++-- spec/views/devise/sessions/new.html.erb_spec.rb | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index a74f67b4df5..29615f1a784 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -102,7 +102,7 @@ <%= javascript_packs_tag_once( 'https://dap.digitalgov.gov/Universal-Federated-Analytics-Min.js?agency=GSA&subagency=TTS', - async: true, + defer: true, id: '_fed_an_ua_tag', preload_links_header: false, ) %> diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 648cf73d3a0..d02278b9932 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -71,7 +71,7 @@ { type: 'application/json', data: { config: '' } }, false, ) %> - <%= javascript_packs_tag_once('track-errors', async: true, preload_links_header: false) if BrowserSupport.supported?(request.user_agent) %> + <%= javascript_packs_tag_once('track-errors', defer: true, preload_links_header: false) if BrowserSupport.supported?(request.user_agent) %> <%= render_javascript_pack_once_tags %> <% end %> diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index 93aa0cae1cf..d10ce5fed41 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -109,7 +109,7 @@ context 'with attributes' do before do - javascript_packs_tag_once('track-errors', async: true) + javascript_packs_tag_once('track-errors', defer: true) allow(Rails.application.config.asset_sources).to receive(:get_sources). with('track-errors').and_return(['/track-errors.js']) allow(Rails.application.config.asset_sources).to receive(:get_assets). @@ -121,7 +121,7 @@ output = render_javascript_pack_once_tags expect(output).to have_css( - "script[src^='/track-errors.js'][async]", + "script[src^='/track-errors.js'][defer]", count: 1, visible: :all, ) diff --git a/spec/views/devise/sessions/new.html.erb_spec.rb b/spec/views/devise/sessions/new.html.erb_spec.rb index af563e5d6c4..f1ef18b13aa 100644 --- a/spec/views/devise/sessions/new.html.erb_spec.rb +++ b/spec/views/devise/sessions/new.html.erb_spec.rb @@ -182,7 +182,7 @@ it 'does not render DAP analytics' do allow(view).to receive(:javascript_packs_tag_once) expect(view).not_to receive(:javascript_packs_tag_once). - with(a_string_matching('https://dap.digitalgov.gov/'), async: true, id: '_fed_an_ua_tag') + with(a_string_matching('https://dap.digitalgov.gov/'), defer: true, id: '_fed_an_ua_tag') render end @@ -195,7 +195,7 @@ allow(view).to receive(:javascript_packs_tag_once) expect(view).to receive(:javascript_packs_tag_once).with( a_string_matching('https://dap.digitalgov.gov/'), - async: true, + defer: true, preload_links_header: false, id: '_fed_an_ua_tag', ) From c44f12a37316b3fde830bcaef24403a45eb02966 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:06:14 -0400 Subject: [PATCH 05/22] LG-14078: Rate-limit backup code attempts based on IP+user ID (#11094) changelog: Internal, Rate Limiting, Enforce additional user IP rate-limiting on backup code submission --- .../backup_code_verification_controller.rb | 4 +- app/forms/backup_code_verification_form.rb | 43 +++++++-- app/services/rate_limiter.rb | 8 ++ config/application.yml.default | 4 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/zh.yml | 1 + lib/identity_config.rb | 4 + .../backup_code_verification_form_spec.rb | 90 ++++++++++++++++++- spec/support/fake_request.rb | 4 + 11 files changed, 149 insertions(+), 12 deletions(-) diff --git a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb index f2fc8e08eb6..e792cc3fc86 100644 --- a/app/controllers/two_factor_authentication/backup_code_verification_controller.rb +++ b/app/controllers/two_factor_authentication/backup_code_verification_controller.rb @@ -16,11 +16,11 @@ def show service_provider: current_sp, remember_device_default: remember_device_default, ) - @backup_code_form = BackupCodeVerificationForm.new(current_user) + @backup_code_form = BackupCodeVerificationForm.new(user: current_user, request:) end def create - @backup_code_form = BackupCodeVerificationForm.new(current_user) + @backup_code_form = BackupCodeVerificationForm.new(user: current_user, request:) result = @backup_code_form.submit(backup_code_params) handle_result(result) end diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index 57e5d14aa14..6c95e530f32 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -3,16 +3,23 @@ class BackupCodeVerificationForm include ActiveModel::Model include ActionView::Helpers::TranslationHelper + include DOTIW::Methods + validate :validate_rate_limited validate :validate_and_consume_backup_code! - def initialize(user) + attr_reader :user, :backup_code, :request + + def initialize(user:, request:) @user = user - @backup_code = '' + @request = request end def submit(params) @backup_code = params[:backup_code] + + rate_limiter.increment! + FormResponse.new( success: valid?, errors:, @@ -21,10 +28,22 @@ def submit(params) ) end - attr_reader :user, :backup_code + private + + def validate_rate_limited + return if !rate_limiter.limited? + errors.add( + :backup_code, + :rate_limited, + message: t( + 'errors.messages.phone_confirmation_limited', + timeout: distance_of_time_in_words(Time.zone.now, rate_limiter.expires_at), + ), + ) + end def validate_and_consume_backup_code! - return if valid_backup_code? + return if rate_limiter.limited? || valid_backup_code? errors.add(:backup_code, :invalid, message: t('two_factor_authentication.invalid_backup_code')) end @@ -38,9 +57,19 @@ def valid_backup_code_config_created_at if_valid_consume_code_return_config_created_at(backup_code) end + def rate_limiter + @rate_limiter ||= RateLimiter.new( + rate_limit_type: :backup_code_user_id_per_ip, + target: [user.id, request.ip].join('-'), + ) + end + def extra_analytics_attributes - { - multi_factor_auth_method_created_at: valid_backup_code_config_created_at&.strftime('%s%L'), - } + { multi_factor_auth_method_created_at: } + end + + def multi_factor_auth_method_created_at + return nil if !valid? + valid_backup_code_config_created_at.strftime('%s%L') end end diff --git a/app/services/rate_limiter.rb b/app/services/rate_limiter.rb index 16b2ce32b70..f4b18ac4fcc 100644 --- a/app/services/rate_limiter.rb +++ b/app/services/rate_limiter.rb @@ -275,6 +275,14 @@ def self.load_rate_limit_config IdentityConfig.store.sign_in_user_id_per_ip_attempt_window_exponential_factor, attempt_window_max: IdentityConfig.store.sign_in_user_id_per_ip_attempt_window_max_minutes, }, + backup_code_user_id_per_ip: { + max_attempts: IdentityConfig.store.backup_code_user_id_per_ip_max_attempts, + attempt_window: IdentityConfig.store.backup_code_user_id_per_ip_attempt_window_in_minutes, + attempt_window_exponential_factor: + IdentityConfig.store.backup_code_user_id_per_ip_attempt_window_exponential_factor, + attempt_window_max: + IdentityConfig.store.backup_code_user_id_per_ip_attempt_window_max_minutes, + }, }.with_indifferent_access end diff --git a/config/application.yml.default b/config/application.yml.default index 84e23fa94e2..d21d14aaade 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -48,6 +48,10 @@ aws_kms_session_key_id: alias/login-dot-gov-test-keymaker aws_logo_bucket: '' aws_region: 'us-west-2' backup_code_cost: '2000$8$1$' +backup_code_user_id_per_ip_attempt_window_exponential_factor: 1.1 +backup_code_user_id_per_ip_attempt_window_in_minutes: 720 +backup_code_user_id_per_ip_attempt_window_max_minutes: 43_200 +backup_code_user_id_per_ip_max_attempts: 50 biometric_ial_enabled: true broken_personal_key_window_finish: '2021-09-22T00:00:00Z' broken_personal_key_window_start: '2021-07-29T00:00:00Z' diff --git a/config/locales/en.yml b/config/locales/en.yml index d1a6ebe5f9a..6de2c54883a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -719,6 +719,7 @@ errors.manage_authenticator.remove_only_method_error: You cannot remove your onl errors.manage_authenticator.unique_name_error: Name already in use. Please use a different name. errors.max_password_attempts_reached: You’ve entered too many incorrect passwords. You can reset your password using the “Forgot your password?” link. errors.messages.already_confirmed: was already confirmed, please try signing in +errors.messages.backup_code_limited: You tried too many times, please try again in %{timeout}. errors.messages.blank: Please fill in this field. errors.messages.blank_cert_element_req: We cannot detect a certificate in your request. errors.messages.confirmation_code_incorrect: Incorrect verification code diff --git a/config/locales/es.yml b/config/locales/es.yml index f6885880c93..6ecef7c4140 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -730,6 +730,7 @@ errors.manage_authenticator.remove_only_method_error: No puede eliminar su únic errors.manage_authenticator.unique_name_error: Ese nombre ya está en uso. Use un nombre diferente. errors.max_password_attempts_reached: Ingresó demasiadas contraseñas incorrectas. Puede restablecer su contraseña usando el vínculo “¿Olvidó su contraseña?”. errors.messages.already_confirmed: ya estaba confirmado, intente iniciar una sesión +errors.messages.backup_code_limited: Lo intentó demasiadas veces; vuelva a intentarlo en %{timeout}. errors.messages.blank: Llene este campo. errors.messages.blank_cert_element_req: No podemos detectar un certificado en su solicitud. errors.messages.confirmation_code_incorrect: Código de verificación incorrecto diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0952d274373..f7cb0fdc66e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -719,6 +719,7 @@ errors.manage_authenticator.remove_only_method_error: Vous ne pouvez pas supprim errors.manage_authenticator.unique_name_error: Ce nom est déjà pris. Veuillez utiliser un nom différent. errors.max_password_attempts_reached: Vous avez saisi des mots de passe inexacts à trop de reprises. Vous pouvez réinitialiser votre mot de passe en utilisant le lien « Mot de passe oublié ? » errors.messages.already_confirmed: a déjà été confirmé, veuillez essayer de vous connecter +errors.messages.backup_code_limited: Vous avez essayé trop de fois, veuillez réessayer dans %{timeout}. errors.messages.blank: Veuillez remplir ce champ. errors.messages.blank_cert_element_req: Nous ne pouvons pas détecter un certificat sur votre demande. errors.messages.confirmation_code_incorrect: Code de vérification incorrect diff --git a/config/locales/zh.yml b/config/locales/zh.yml index b1bb98455c6..0122bd5cb22 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -730,6 +730,7 @@ errors.manage_authenticator.remove_only_method_error: 你不能去掉自己唯 errors.manage_authenticator.unique_name_error: 名字已在使用。请使用一个不同的名字。 errors.max_password_attempts_reached: 你输入了太多不正确的密码。你可以使用“忘了密码?”链接来重设密码。 errors.messages.already_confirmed: 已确认,请尝试登录 +errors.messages.backup_code_limited: 你尝试了太多次。请在 %{timeout}后再试。 errors.messages.blank: 请填写这一字段。 errors.messages.blank_cert_element_req: 我们在你的请求中探查不到证书。 errors.messages.confirmation_code_incorrect: 验证码不对。你打字打对了吗? diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 593f536d53d..9412d21a65f 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -65,6 +65,10 @@ def self.store config.add(:aws_logo_bucket, type: :string) config.add(:aws_region, type: :string) config.add(:backup_code_cost, type: :string) + config.add(:backup_code_user_id_per_ip_attempt_window_exponential_factor, type: :float) + config.add(:backup_code_user_id_per_ip_attempt_window_in_minutes, type: :integer) + config.add(:backup_code_user_id_per_ip_attempt_window_max_minutes, type: :integer) + config.add(:backup_code_user_id_per_ip_max_attempts, type: :integer) config.add(:biometric_ial_enabled, type: :boolean) config.add(:broken_personal_key_window_finish, type: :timestamp) config.add(:broken_personal_key_window_start, type: :timestamp) diff --git a/spec/forms/backup_code_verification_form_spec.rb b/spec/forms/backup_code_verification_form_spec.rb index 58708037c20..fca3a992ce3 100644 --- a/spec/forms/backup_code_verification_form_spec.rb +++ b/spec/forms/backup_code_verification_form_spec.rb @@ -1,9 +1,13 @@ require 'rails_helper' RSpec.describe BackupCodeVerificationForm do - subject(:result) { described_class.new(user).submit(params).to_h } + include DOTIW::Methods + subject(:result) { form.submit(params) } + + let(:form) { described_class.new(user:, request:) } let(:user) { create(:user) } + let(:request) { FakeRequest.new } let(:backup_codes) { BackupCodeGenerator.new(user).delete_and_regenerate } let(:backup_code_config) do BackupCodeConfiguration.find_with_code(code: code, user_id: user.id) @@ -16,7 +20,7 @@ let(:code) { backup_codes.first } it 'returns success' do - expect(result).to eq( + expect(result.to_h).to eq( success: true, multi_factor_auth_method_created_at: backup_code_config.created_at.strftime('%s%L'), ) @@ -34,12 +38,92 @@ let(:code) { 'invalid' } it 'returns failure' do - expect(result).to eq( + expect(result.first_error_message).to eq(t('two_factor_authentication.invalid_backup_code')) + expect(result.to_h).to eq( success: false, error_details: { backup_code: { invalid: true } }, multi_factor_auth_method_created_at: nil, ) end end + + describe 'rate limiting', :freeze_time do + before do + allow(RateLimiter).to receive(:rate_limit_config).and_return( + backup_code_user_id_per_ip: { + max_attempts: 2, + attempt_window: 60, + attempt_window_exponential_factor: 3, + attempt_window_max: 12.hours.in_minutes, + }, + ) + end + + context 'before hitting rate limit' do + context 'with an invalid code' do + let(:code) { 'invalid' } + + it 'returns failure due to invalid code' do + expect(result.first_error_message).to eq( + t('two_factor_authentication.invalid_backup_code'), + ) + expect(result.to_h).to eq( + success: false, + error_details: { backup_code: { invalid: true } }, + multi_factor_auth_method_created_at: nil, + ) + end + end + end + + context 'after hitting rate limit' do + before do + form.submit(params.merge(backup_code: 'invalid')) + end + + context 'with an invalid code' do + let(:code) { 'invalid' } + + it 'returns failure due to rate limiting' do + expect(result.first_error_message).to eq( + t( + 'errors.messages.phone_confirmation_limited', + timeout: distance_of_time_in_words(3.hours), + ), + ) + expect(result.to_h).to eq( + success: false, + error_details: { backup_code: { rate_limited: true } }, + multi_factor_auth_method_created_at: nil, + ) + end + end + + context 'with a valid code' do + let(:code) { backup_codes.first } + + it 'returns failure due to rate limiting' do + expect(result.first_error_message).to eq( + t( + 'errors.messages.phone_confirmation_limited', + timeout: distance_of_time_in_words(3.hours), + ), + ) + expect(result.to_h).to eq( + success: false, + error_details: { backup_code: { rate_limited: true } }, + multi_factor_auth_method_created_at: nil, + ) + end + + it 'does not consume code' do + result + + configuration = BackupCodeConfiguration.find_with_code(code:, user_id: user.id) + expect(configuration.used_at).to be_blank + end + end + end + end end end diff --git a/spec/support/fake_request.rb b/spec/support/fake_request.rb index 868ae19945a..dee732547b6 100644 --- a/spec/support/fake_request.rb +++ b/spec/support/fake_request.rb @@ -5,6 +5,10 @@ def initialize(headers: {}) @headers = headers end + def ip + '127.0.0.1' + end + def remote_ip '127.0.0.1' end From 12da96cacc3ddcc7e01b635eb5e96c152850c1e2 Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:17:38 -0400 Subject: [PATCH 06/22] LG-14175: Expire EIPP enrollments in job (#11085) * ensure that enhanced ipp enrollments are expired * Changelog: User-Facing Improvements, In-person Pr oofing, ensure eipp enrollments are expired * add specs for when enrollments aren't being expired * update mock proofer to support manual testing of expired eipp enrollments * Fix mock proofer to return correct error for expired eipp enrollments --- .../usps_in_person_proofing/mock/fixtures.rb | 8 ++- .../usps_in_person_proofing/mock/proofer.rb | 10 ++- ...expired_enhanced_ipp_results_response.json | 3 + ...uest_expired_id_ipp_results_response.json} | 0 .../get_usps_proofing_results_job_spec.rb | 69 ++++++++++++++++--- spec/support/usps_ipp_helper.rb | 22 ++++-- 6 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 app/services/usps_in_person_proofing/mock/responses/request_expired_enhanced_ipp_results_response.json rename app/services/usps_in_person_proofing/mock/responses/{request_expired_proofing_results_response.json => request_expired_id_ipp_results_response.json} (100%) diff --git a/app/services/usps_in_person_proofing/mock/fixtures.rb b/app/services/usps_in_person_proofing/mock/fixtures.rb index 264405aa734..9dec54b1293 100644 --- a/app/services/usps_in_person_proofing/mock/fixtures.rb +++ b/app/services/usps_in_person_proofing/mock/fixtures.rb @@ -83,8 +83,12 @@ def self.request_passed_proofing_secondary_id_type_results_response_ial_2 ) end - def self.request_expired_proofing_results_response - load_response_fixture('request_expired_proofing_results_response.json') + def self.request_expired_enhanced_ipp_results_response + load_response_fixture('request_expired_enhanced_ipp_results_response.json') + end + + def self.request_expired_id_ipp_results_response + load_response_fixture('request_expired_id_ipp_results_response.json') end def self.request_unexpected_expired_proofing_results_response diff --git a/app/services/usps_in_person_proofing/mock/proofer.rb b/app/services/usps_in_person_proofing/mock/proofer.rb index a7a55e9d136..21daab7051f 100644 --- a/app/services/usps_in_person_proofing/mock/proofer.rb +++ b/app/services/usps_in_person_proofing/mock/proofer.rb @@ -40,8 +40,14 @@ def request_facilities(_location, is_enhanced_ipp) end end - def request_proofing_results(_enrollment) - JSON.parse(Fixtures.request_passed_proofing_results_response) + def request_proofing_results(enrollment) + if enrollment.days_to_due_date.negative? && enrollment.enhanced_ipp? + body = JSON.parse(Fixtures.request_expired_enhanced_ipp_results_response) + response = { body: body, status: 400 } + raise Faraday::BadRequestError.new('Bad request error', response) + else + JSON.parse(Fixtures.request_passed_proofing_results_response) + end end end end diff --git a/app/services/usps_in_person_proofing/mock/responses/request_expired_enhanced_ipp_results_response.json b/app/services/usps_in_person_proofing/mock/responses/request_expired_enhanced_ipp_results_response.json new file mode 100644 index 00000000000..16d85f1cb11 --- /dev/null +++ b/app/services/usps_in_person_proofing/mock/responses/request_expired_enhanced_ipp_results_response.json @@ -0,0 +1,3 @@ +{ + "responseMessage": "More than 7 days have passed since opt-in to IPP" +} diff --git a/app/services/usps_in_person_proofing/mock/responses/request_expired_proofing_results_response.json b/app/services/usps_in_person_proofing/mock/responses/request_expired_id_ipp_results_response.json similarity index 100% rename from app/services/usps_in_person_proofing/mock/responses/request_expired_proofing_results_response.json rename to app/services/usps_in_person_proofing/mock/responses/request_expired_id_ipp_results_response.json diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 7394e34a475..9415eacf5ba 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -366,7 +366,7 @@ request_in_progress_proofing_results_args, { status: 500 }, request_failed_proofing_results_args, - request_expired_proofing_results_args, + request_expired_id_ipp_results_args, ).and_raise(Faraday::TimeoutError).and_raise(Faraday::ConnectionFailed) job.perform(Time.zone.now) @@ -527,7 +527,7 @@ end it 'sends deadline passed email on response with expired status' do - stub_request_expired_proofing_results + stub_request_expired_id_ipp_proofing_results user = pending_enrollment.user expect(pending_enrollment.deadline_passed_sent).to be false freeze_time do @@ -851,7 +851,7 @@ context 'when an enrollment expires' do before(:each) do - stub_request_expired_proofing_results + stub_request_expired_id_ipp_proofing_results end it_behaves_like( @@ -860,7 +860,7 @@ email_type: 'deadline passed', enrollment_status: InPersonEnrollment::STATUS_EXPIRED, response_json: UspsInPersonProofing::Mock::Fixtures. - request_expired_proofing_results_response, + request_expired_id_ipp_results_response, ) it 'logs that the enrollment expired' do @@ -1229,7 +1229,7 @@ end it 'does not set the fraud related fields of an expired enrollment' do - stub_request_expired_proofing_results + stub_request_expired_id_ipp_proofing_results job.perform(Time.zone.now) profile = pending_enrollment.reload.profile @@ -1354,7 +1354,7 @@ end it 'deactivates and sets fraud related fields of an expired enrollment' do - stub_request_expired_proofing_results + stub_request_expired_id_ipp_proofing_results job.perform(Time.zone.now) @@ -1449,7 +1449,7 @@ context 'enrollment is expired' do it 'deletes the notification phone configuration without sending an sms' do - stub_request_expired_proofing_results + stub_request_expired_id_ipp_proofing_results expect(pending_enrollment.notification_phone_configuration).to_not be_nil @@ -1531,7 +1531,7 @@ end context <<~STR.squish do - When an Enhanced IPP enrollment passess proofing + When an Enhanced IPP enrollment passes proofing with unsupported ID,enrollment by-passes the Primary ID check and STR @@ -1585,7 +1585,7 @@ end end - context 'By passes the Secondary ID check when enrollment is Enhanced IPP' do + context 'Bypasses the Secondary ID check when enrollment is Enhanced IPP' do before do stub_request_passed_proofing_secondary_id_type_results_ial_2 end @@ -1600,6 +1600,57 @@ enhanced_ipp_enrollment: true, ) end + + context 'when an enrollment expires' do + before(:each) do + stub_request_expired_enhanced_ipp_proofing_results + end + + it_behaves_like( + 'enrollment_with_a_status_update', + passed: false, + email_type: 'deadline passed', + enrollment_status: InPersonEnrollment::STATUS_EXPIRED, + response_json: UspsInPersonProofing::Mock::Fixtures. + request_expired_enhanced_ipp_results_response, + enhanced_ipp_enrollment: true, + ) + + it 'logs that the enrollment expired' do + job.perform(Time.zone.now) + + expect(pending_enrollment.proofed_at).to eq(nil) + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Enrollment status updated', + hash_including( + reason: 'Enrollment has expired', + job_name: 'GetUspsProofingResultsJob', + enhanced_ipp: true, + ), + ) + end + + context 'when the in_person_stop_expiring_enrollments flag is true' do + before do + allow(IdentityConfig.store).to( + receive(:in_person_stop_expiring_enrollments).and_return(true), + ) + end + + it 'treats the enrollment as incomplete' do + job.perform(Time.zone.now) + + expect(pending_enrollment.status).to eq(InPersonEnrollment::STATUS_PENDING) + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Enrollment incomplete', + hash_including( + response_message: 'More than 7 days have passed since opt-in to IPP', + job_name: 'GetUspsProofingResultsJob', + ), + ) + end + end + end end end diff --git a/spec/support/usps_ipp_helper.rb b/spec/support/usps_ipp_helper.rb index 03fb458c49b..8f22c7105ad 100644 --- a/spec/support/usps_ipp_helper.rb +++ b/spec/support/usps_ipp_helper.rb @@ -121,16 +121,30 @@ def stub_request_enroll_non_hash_response ) end - def stub_request_expired_proofing_results + def stub_request_expired_enhanced_ipp_proofing_results stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( - **request_expired_proofing_results_args, + **request_expired_enhanced_ipp_results_args, ) end - def request_expired_proofing_results_args + def request_expired_enhanced_ipp_results_args { status: 400, - body: UspsInPersonProofing::Mock::Fixtures.request_expired_proofing_results_response, + body: UspsInPersonProofing::Mock::Fixtures.request_expired_enhanced_ipp_results_response, + headers: { 'content-type' => 'application/json' }, + } + end + + def stub_request_expired_id_ipp_proofing_results + stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}).to_return( + **request_expired_id_ipp_results_args, + ) + end + + def request_expired_id_ipp_results_args + { + status: 400, + body: UspsInPersonProofing::Mock::Fixtures.request_expired_id_ipp_results_response, headers: { 'content-type' => 'application/json' }, } end From b5b9e5ffd809f9c27aa9255427d22f1c79773662 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:44:21 -0400 Subject: [PATCH 07/22] Document analytics methods properties in primary IdV flow (#11099) * Document analytics methods in critical IdV flow * Remove unnecessary allowed_extra_analytics * Update FakeAnalytics spec to pass required properties * Add changelog changelog: Internal, Documentation, Document analytics methods properties --- app/services/analytics_events.rb | 3 +++ spec/controllers/idv/image_uploads_controller_spec.rb | 2 +- spec/features/idv/account_creation_spec.rb | 2 +- spec/features/idv/cancel_spec.rb | 2 +- spec/features/idv/doc_auth/ssn_step_spec.rb | 2 +- spec/features/idv/gpo_disabled_spec.rb | 2 +- spec/features/idv/phone_input_spec.rb | 2 +- spec/features/idv/proofing_components_spec.rb | 2 +- spec/features/idv/sp_requested_attributes_spec.rb | 2 +- spec/features/idv/step_up_spec.rb | 2 +- spec/features/idv/steps/phone_otp_verification_step_spec.rb | 2 +- spec/features/idv/threat_metrix_pending_spec.rb | 2 +- spec/features/idv/uak_password_spec.rb | 2 +- spec/features/sp_cost_tracking_spec.rb | 2 +- .../features/users/password_recovery_via_recovery_code_spec.rb | 2 +- spec/support/fake_analytics_spec.rb | 1 + 16 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 45edc502c4a..87b6c76bc44 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1478,6 +1478,7 @@ def idv_doc_auth_submitted_image_upload_form( # @param [Boolean] selfie_quality_good Selfie quality result # @param [String] workflow LexisNexis TrueID workflow # @param [String] birth_year Birth year from document + # @param [Integer] issue_year Year document was issued # @option extra [String] 'DocumentName' # @option extra [String] 'DocAuthResult' # @option extra [String] 'DocIssuerCode' @@ -1505,6 +1506,7 @@ def idv_doc_auth_submitted_image_upload_vendor( client_image_metrics:, flow_path:, liveness_checking_required:, + issue_year:, billed: nil, doc_auth_result: nil, vendor_request_time_in_ms: nil, @@ -1578,6 +1580,7 @@ def idv_doc_auth_submitted_image_upload_vendor( selfie_quality_good:, workflow:, birth_year:, + issue_year:, **extra, ) end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 7f88b87a450..6dc3d4cb7e4 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Idv::ImageUploadsController, allowed_extra_analytics: [:*] do +RSpec.describe Idv::ImageUploadsController do include DocPiiHelper let(:document_filename_regex) { /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}\.[a-z]+$/ } diff --git a/spec/features/idv/account_creation_spec.rb b/spec/features/idv/account_creation_spec.rb index ffb7475f902..76bf8d6fc12 100644 --- a/spec/features/idv/account_creation_spec.rb +++ b/spec/features/idv/account_creation_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'IAL2 account creation', allowed_extra_analytics: [:*] do +RSpec.describe 'IAL2 account creation' do include IdvHelper include DocAuthHelper include SamlAuthHelper diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb index 0eddb6e18d0..158eadf7835 100644 --- a/spec/features/idv/cancel_spec.rb +++ b/spec/features/idv/cancel_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'cancel IdV', allowed_extra_analytics: [:*] do +RSpec.describe 'cancel IdV' do include IdvStepHelper include DocAuthHelper include InteractionHelper diff --git a/spec/features/idv/doc_auth/ssn_step_spec.rb b/spec/features/idv/doc_auth/ssn_step_spec.rb index 36c5388e991..e71d256f10e 100644 --- a/spec/features/idv/doc_auth/ssn_step_spec.rb +++ b/spec/features/idv/doc_auth/ssn_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'ssn step mock proofer', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'ssn step mock proofer', :js do include IdvStepHelper include DocAuthHelper diff --git a/spec/features/idv/gpo_disabled_spec.rb b/spec/features/idv/gpo_disabled_spec.rb index 6830557ba10..859ded499fc 100644 --- a/spec/features/idv/gpo_disabled_spec.rb +++ b/spec/features/idv/gpo_disabled_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'disabling GPO address verification', allowed_extra_analytics: [:*] do +RSpec.feature 'disabling GPO address verification' do include IdvStepHelper context 'with GPO address verification disabled' do diff --git a/spec/features/idv/phone_input_spec.rb b/spec/features/idv/phone_input_spec.rb index aa5e7ab07b1..1dfcf9fc3c1 100644 --- a/spec/features/idv/phone_input_spec.rb +++ b/spec/features/idv/phone_input_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'IdV phone number input', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'IdV phone number input', :js do include IdvStepHelper before do diff --git a/spec/features/idv/proofing_components_spec.rb b/spec/features/idv/proofing_components_spec.rb index 9849497c69d..a12f58ad57f 100644 --- a/spec/features/idv/proofing_components_spec.rb +++ b/spec/features/idv/proofing_components_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'proofing components', allowed_extra_analytics: [:*] do +RSpec.describe 'proofing components' do include DocAuthHelper include IdvHelper include SamlAuthHelper diff --git a/spec/features/idv/sp_requested_attributes_spec.rb b/spec/features/idv/sp_requested_attributes_spec.rb index c8b1ef926e6..4140120f3fb 100644 --- a/spec/features/idv/sp_requested_attributes_spec.rb +++ b/spec/features/idv/sp_requested_attributes_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'sp requested IdV attributes', :email, allowed_extra_analytics: [:*] do +RSpec.feature 'sp requested IdV attributes', :email do context 'oidc' do it_behaves_like 'sp requesting attributes', :oidc end diff --git a/spec/features/idv/step_up_spec.rb b/spec/features/idv/step_up_spec.rb index 98348efb05d..e00732f1a8b 100644 --- a/spec/features/idv/step_up_spec.rb +++ b/spec/features/idv/step_up_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'IdV step up flow', allowed_extra_analytics: [:*] do +RSpec.describe 'IdV step up flow' do include IdvStepHelper include InPersonHelper diff --git a/spec/features/idv/steps/phone_otp_verification_step_spec.rb b/spec/features/idv/steps/phone_otp_verification_step_spec.rb index 06420ce6e87..5b397d0fcfd 100644 --- a/spec/features/idv/steps/phone_otp_verification_step_spec.rb +++ b/spec/features/idv/steps/phone_otp_verification_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'phone otp verification step spec', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'phone otp verification step spec', :js do include IdvStepHelper it 'requires the user to enter the correct otp before continuing' do diff --git a/spec/features/idv/threat_metrix_pending_spec.rb b/spec/features/idv/threat_metrix_pending_spec.rb index e5a1b5b8bcf..43686859d25 100644 --- a/spec/features/idv/threat_metrix_pending_spec.rb +++ b/spec/features/idv/threat_metrix_pending_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Users pending ThreatMetrix review', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'Users pending ThreatMetrix review', :js do include IdvStepHelper include OidcAuthHelper include DocAuthHelper diff --git a/spec/features/idv/uak_password_spec.rb b/spec/features/idv/uak_password_spec.rb index ec66f31775e..61bdbd335ee 100644 --- a/spec/features/idv/uak_password_spec.rb +++ b/spec/features/idv/uak_password_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'A user with a UAK passwords attempts IdV', allowed_extra_analytics: [:*] do +RSpec.feature 'A user with a UAK passwords attempts IdV' do include IdvStepHelper it 'allows the user to continue to the SP', js: true do diff --git a/spec/features/sp_cost_tracking_spec.rb b/spec/features/sp_cost_tracking_spec.rb index 8bf74757b6a..9526068ac37 100644 --- a/spec/features/sp_cost_tracking_spec.rb +++ b/spec/features/sp_cost_tracking_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'SP Costing', :email, allowed_extra_analytics: [:*] do +RSpec.feature 'SP Costing', :email do include SpAuthHelper include SamlAuthHelper include IdvHelper diff --git a/spec/features/users/password_recovery_via_recovery_code_spec.rb b/spec/features/users/password_recovery_via_recovery_code_spec.rb index 6c755692ba0..9af1df8b475 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'Password recovery via personal key', allowed_extra_analytics: [:*] do +RSpec.feature 'Password recovery via personal key' do include PersonalKeyHelper include IdvStepHelper include SamlAuthHelper diff --git a/spec/support/fake_analytics_spec.rb b/spec/support/fake_analytics_spec.rb index 381aa6fa95f..fb3533c0cc8 100644 --- a/spec/support/fake_analytics_spec.rb +++ b/spec/support/fake_analytics_spec.rb @@ -621,6 +621,7 @@ client_image_metrics: nil, flow_path: nil, liveness_checking_required: nil, + issue_year: nil, 'DocumentName' => 'some_name', ) From e92a99f5f3c93c011ef4e8a8d4e0b98be1435a6e Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:46:15 -0400 Subject: [PATCH 08/22] Update Propshaft to 0.9.x (#11103) * Update Propshaft to 0.9.x changelog: Internal, Dependencies, Update dependencies to latest versions * Remove monkeypatch file reference --- Gemfile.lock | 2 +- config/initializers/assets.rb | 2 -- lib/extensions/propshaft/asset.rb | 13 ------------- 3 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 lib/extensions/propshaft/asset.rb diff --git a/Gemfile.lock b/Gemfile.lock index ed7ed0c8b91..82808af2c85 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -479,7 +479,7 @@ GEM profanity_filter (0.1.1) prometheus_exporter (2.1.0) webrick - propshaft (0.7.0) + propshaft (0.9.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 6f1ca9f3022..4f2630bea56 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -2,8 +2,6 @@ # Be sure to restart your server when you modify this file. -require 'extensions/propshaft/asset' - # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' diff --git a/lib/extensions/propshaft/asset.rb b/lib/extensions/propshaft/asset.rb deleted file mode 100644 index 7acc19c3758..00000000000 --- a/lib/extensions/propshaft/asset.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# Monkey-patch Propshaft::Asset to produce a shorter digest as an optimization to built output size. -# -# See: https://github.com/rails/propshaft/blob/main/lib/propshaft/asset.rb - -module Extensions - Propshaft::Asset.class_eval do - def digest - @digest ||= Digest::SHA1.hexdigest("#{content}#{version}")[0...8] - end - end -end From 2c8195024f30fb12dc72913cd81f42d667b018e1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:36:53 -0400 Subject: [PATCH 09/22] Try: Fix second MFA reminder flakey spec (#11109) changelog: Internal, Automated Testing, Improve reliability of automated tests --- .../second_mfa_reminder_spec.rb | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/spec/features/two_factor_authentication/second_mfa_reminder_spec.rb b/spec/features/two_factor_authentication/second_mfa_reminder_spec.rb index b2d15ec3e93..af9cc0a48ff 100644 --- a/spec/features/two_factor_authentication/second_mfa_reminder_spec.rb +++ b/spec/features/two_factor_authentication/second_mfa_reminder_spec.rb @@ -8,7 +8,6 @@ before do allow(IdentityConfig.store).to receive(:second_mfa_reminder_sign_in_count).and_return(2) - allow(IdentityConfig.store).to receive(:second_mfa_reminder_account_age_in_days).and_return(5) IdentityLinker.new(user, service_provider).link_identity(verified_attributes: %w[openid email]) end @@ -46,7 +45,11 @@ end context 'after age threshold' do - before { travel 6.days } + before do + user.update( + created_at: (IdentityConfig.store.second_mfa_reminder_account_age_in_days + 1).days.ago, + ) + end it 'prompts the user on sign in and allows them to add an authentication method' do sign_in_user(user) @@ -57,31 +60,34 @@ expect(page).to have_current_path(authentication_methods_setup_url) end - end - context 'user already acknowledged reminder' do - before do - travel 6.days - sign_in_user(user) - fill_in_code_with_last_phone_otp - click_submit_default - click_button t('users.second_mfa_reminder.continue', sp_name: APP_NAME) - first(:button, t('links.sign_out')).click - end + context 'user already acknowledged reminder' do + before do + sign_in_user(user) + fill_in_code_with_last_phone_otp + click_submit_default + click_button t('users.second_mfa_reminder.continue', sp_name: APP_NAME) + first(:button, t('links.sign_out')).click + end - it 'does not prompt the user on sign in' do - sign_in_user(user) + it 'does not prompt the user on sign in' do + sign_in_user(user) - expect(page).to have_current_path(account_path) + expect(page).to have_current_path(account_path) + end end end end context 'user with multiple mfas who would otherwise be candidate' do - let(:user) { create(:user, :fully_registered, :with_phone, :with_authentication_app) } - - before do - travel 6.days + let(:user) do + create( + :user, + :fully_registered, + :with_phone, + :with_authentication_app, + created_at: (IdentityConfig.store.second_mfa_reminder_account_age_in_days + 1).days.ago, + ) end it 'does not prompt the user on sign in' do From 0c0e6434b4cf9d1ea7b35f3498df55894e4297e3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:38:10 -0400 Subject: [PATCH 10/22] Remove reference to frontend interest group team in contributing guide (#11108) changelog: Internal, Documentation, Remove reference to frontend interest group team in contributing guide --- CONTRIBUTING.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0af088f5112..cceb0216091 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,12 +90,7 @@ commits together, merges it, then deletes the branch. Everyone is encouraged to participate in code review. To solicit feedback from specific people, consider adding individuals or groups as requested reviewers on your pull request. Most internal -product teams have a team handle which can be used to notify everyone on that team, or you can -request reviews from one of the available interest group teams: - -- `18f/identity-frontend` for developers interested in frontend development - -To request to join any of these teams, you can contact any existing member and ask to be added. +product teams have a team handle which can be used to notify everyone on that team. ## Public domain From 08a228775fcf5e36ffef403d5672acc568b479b1 Mon Sep 17 00:00:00 2001 From: A Shukla Date: Mon, 19 Aug 2024 09:04:54 -0500 Subject: [PATCH 11/22] Renamed variable (#11102) * Renamed variable * Changed value to non mutation of variable in all occurences, resolve PR comments * changelog: internal, code format, changed variable name --- .../idv/doc_auth/document_capture_spec.rb | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 0e13d9211a5..1fbc6e82b87 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -43,14 +43,14 @@ message = strip_tags(t('doc_auth.errors.doc_type_not_supported_heading')) expect(page).to have_content(message) detail_message = strip_tags(t('doc_auth.errors.doc.doc_type_check')) - security_message = strip_tags( + warning_message = strip_tags( t( 'idv.failure.attempts_html', count: IdentityConfig.store.doc_auth_max_attempts - 1, ), ) expect(page).to have_content(detail_message) - expect(page).to have_content(security_message) + expect(page).to have_content(warning_message) expect(page).to have_current_path(idv_document_capture_path) click_try_again expect(page).to have_current_path(idv_document_capture_path) @@ -1014,13 +1014,13 @@ message = strip_tags(t('doc_auth.errors.selfie_not_live_or_poor_quality_heading')) expect(page).to have_content(message) detail_message = strip_tags(t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality')) - security_message = strip_tags( + warning_message = strip_tags( t( 'idv.failure.attempts_html', count: IdentityConfig.store.doc_auth_max_attempts - 1, ), ) - expect(page).to have_content(detail_message << "\n" << security_message) + expect(page).to have_content("#{detail_message}\n#{warning_message}") review_issues_header = strip_tags( t('doc_auth.errors.selfie_not_live_or_poor_quality_heading'), ) @@ -1052,13 +1052,13 @@ message = strip_tags(t('doc_auth.errors.selfie_not_live_or_poor_quality_heading')) expect(page).to have_content(message) detail_message = strip_tags(t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality')) - security_message = strip_tags( + warning_message = strip_tags( t( 'idv.failure.attempts_html', count: IdentityConfig.store.doc_auth_max_attempts - 1, ), ) - expect(page).to have_content(detail_message << "\n" << security_message) + expect(page).to have_content("#{detail_message}\n#{warning_message}") review_issues_header = strip_tags( t('doc_auth.errors.selfie_not_live_or_poor_quality_heading'), ) @@ -1090,13 +1090,13 @@ message = strip_tags(t('doc_auth.errors.selfie_fail_heading')) expect(page).to have_content(message) detail_message = strip_tags(t('doc_auth.errors.general.selfie_failure')) - security_message = strip_tags( + warning_message = strip_tags( t( 'idv.failure.attempts_html', count: IdentityConfig.store.doc_auth_max_attempts - 1, ), ) - expect(page).to have_content(detail_message << "\n" << security_message) + expect(page).to have_content("#{detail_message}\n#{warning_message}") review_issues_header = strip_tags( t('doc_auth.errors.selfie_fail_heading'), ) @@ -1129,14 +1129,13 @@ message = strip_tags(t('doc_auth.errors.selfie_not_live_or_poor_quality_heading')) expect(page).to have_content(message) detail_message = strip_tags(t('doc_auth.errors.alerts.selfie_not_live_or_poor_quality')) - security_message = strip_tags( + warning_message = strip_tags( t( 'idv.failure.attempts_html', count: IdentityConfig.store.doc_auth_max_attempts - 1, ), ) - - expect(page).to have_content(detail_message << "\n" << security_message) + expect(page).to have_content("#{detail_message}\n#{warning_message}") review_issues_header = strip_tags( t('doc_auth.errors.selfie_not_live_or_poor_quality_heading'), ) From 0923f956485381438c21d8c1ec62dae19f97c2f1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:11:28 -0400 Subject: [PATCH 12/22] Enforce YAML normalization for application.yml.default (#11106) changelog: Internal, Automated Testing, Enforce YAML normalization for application.yml.default --- Makefile | 2 +- config/application.yml.default | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index c8f98404a75..b779ffaf0de 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ lint_erb: ## Lints ERB files bundle exec erblint app/views app/components lint_yaml: normalize_yaml ## Lints YAML files - (! git diff --name-only | grep "^config/.*\.yml$$") || (echo "Error: Run 'make normalize_yaml' to normalize YAML"; exit 1) + (! git diff --name-only | grep "^config/.*\.yml") || (echo "Error: Run 'make normalize_yaml' to normalize YAML"; exit 1) lint_font_glyphs: ## Lints to validate content glyphs match expectations from fonts scripts/yaml_characters \ diff --git a/config/application.yml.default b/config/application.yml.default index d21d14aaade..47f54bdcde8 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -64,20 +64,20 @@ country_phone_number_overrides: '{}' database_host: '' database_name: '' database_password: '' +database_pool_idp: 5 database_read_replica_host: '' database_readonly_password: '' database_readonly_username: '' -database_username: '' -database_worker_jobs_host: '' -database_worker_jobs_name: '' -database_worker_jobs_password: '' -database_worker_jobs_username: '' -database_pool_idp: 5 database_socket: '' database_sslmode: 'verify-full' database_statement_timeout: 2_500 database_timeout: 5_000 +database_username: '' +database_worker_jobs_host: '' +database_worker_jobs_name: '' +database_worker_jobs_password: '' database_worker_jobs_sslmode: 'verify-full' +database_worker_jobs_username: '' deleted_user_accounts_report_configs: '[]' deliver_mail_async: false development_mailer_deliver_method: letter_opener From e101d3377f7ced3391f5ad68bcc6a6718768f430 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Mon, 19 Aug 2024 10:39:41 -0400 Subject: [PATCH 13/22] Jmax/lg 13875 rename documentstep in doc auth (#11092) * Renamed component DocumentsStep -> DocumentsAndSelfieStep Renamed files to match new component name. Because policy, the rename meant moving from a .jsx file to a .tsx file, so there was some Typescript cleanup to do; it amounted to adding dummy arguments to component rendering in the specs. changelog: Upcoming Features,biometric verification,Renamed `DocumentsStep` to `DocumentsAndSelfieStep` --- .../document-capture-review-issues.tsx | 2 +- .../components/document-capture.tsx | 4 +- ...step.tsx => documents-and-selfie-step.tsx} | 10 +-- scripts/enforce-typescript-files.mjs | 1 - ...jsx => documents-and-selfie-step-spec.tsx} | 78 ++++++++++++++++--- 5 files changed, 73 insertions(+), 22 deletions(-) rename app/javascript/packages/document-capture/components/{documents-step.tsx => documents-and-selfie-step.tsx} (95%) rename spec/javascript/packages/document-capture/components/{documents-step-spec.jsx => documents-and-selfie-step-spec.tsx} (71%) diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index af3547394dc..3ccbe0454cf 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -11,7 +11,7 @@ import { DocumentCaptureSubheaderOne, SelfieCaptureWithHeader, DocumentFrontAndBackCapture, -} from './documents-step'; +} from './documents-and-selfie-step'; import type { ReviewIssuesStepValue } from './review-issues-step'; interface DocumentCaptureReviewIssuesProps extends FormStepComponentProps { diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index afbacbc27d6..5bfe414d3d7 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -7,7 +7,7 @@ import { useDidUpdateEffect } from '@18f/identity-react-hooks'; import type { FormStep } from '@18f/identity-form-steps'; import { getConfigValue } from '@18f/identity-config'; import { UploadFormEntriesError } from '../services/upload'; -import DocumentsStep from './documents-step'; +import DocumentsAndSelfieStep from './documents-and-selfie-step'; import InPersonPrepareStep from './in-person-prepare-step'; import InPersonLocationPostOfficeSearchStep from './in-person-location-post-office-search-step'; import InPersonLocationFullAddressEntryPostOfficeSearchStep from './in-person-location-full-address-entry-post-office-search-step'; @@ -53,7 +53,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { // Define different states to be used in human readable array declaration const documentFormStep: FormStep = { name: 'documents', - form: DocumentsStep, + form: DocumentsAndSelfieStep, title: t('doc_auth.headings.document_capture'), }; const reviewFormStep: FormStep = { diff --git a/app/javascript/packages/document-capture/components/documents-step.tsx b/app/javascript/packages/document-capture/components/documents-and-selfie-step.tsx similarity index 95% rename from app/javascript/packages/document-capture/components/documents-step.tsx rename to app/javascript/packages/document-capture/components/documents-and-selfie-step.tsx index 69da4edb4f2..f0ae2ec5ede 100644 --- a/app/javascript/packages/document-capture/components/documents-step.tsx +++ b/app/javascript/packages/document-capture/components/documents-and-selfie-step.tsx @@ -84,7 +84,7 @@ export function DocumentFrontAndBackCapture({ type ImageValue = Blob | string | null | undefined; -interface DocumentsStepValue { +interface DocumentsAndSelfieStepValue { front: ImageValue; back: ImageValue; selfie: ImageValue; @@ -93,17 +93,17 @@ interface DocumentsStepValue { } type DefaultSideProps = Pick< - FormStepComponentProps, + FormStepComponentProps, 'registerField' | 'onChange' | 'errors' | 'onError' >; -function DocumentsStep({ +export default function DocumentsAndSelfieStep({ value = {}, onChange = () => {}, errors = [], onError = () => {}, registerField = () => undefined, -}: FormStepComponentProps) { +}: FormStepComponentProps) { const { t } = useI18n(); const { isMobile } = useContext(DeviceContext); const { isLastStep } = useContext(FormStepsContext); @@ -145,5 +145,3 @@ function DocumentsStep({ ); } - -export default DocumentsStep; diff --git a/scripts/enforce-typescript-files.mjs b/scripts/enforce-typescript-files.mjs index 02ef7efe969..6496502e94d 100755 --- a/scripts/enforce-typescript-files.mjs +++ b/scripts/enforce-typescript-files.mjs @@ -47,7 +47,6 @@ const LEGACY_FILE_EXCEPTIONS = [ 'spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx', 'spec/javascript/packages/document-capture/components/document-capture-spec.jsx', 'spec/javascript/packages/document-capture/components/document-capture-warning-spec.jsx', - 'spec/javascript/packages/document-capture/components/documents-step-spec.jsx', 'spec/javascript/packages/document-capture/components/file-image-spec.jsx', 'spec/javascript/packages/document-capture/components/file-input-spec.jsx', 'spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx', diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx similarity index 71% rename from spec/javascript/packages/document-capture/components/documents-step-spec.jsx rename to spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx index 8f0f2a03605..482ad5ca2be 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/documents-and-selfie-step-spec.tsx @@ -9,14 +9,24 @@ import { FailedCaptureAttemptsContextProvider, SelfieCaptureContext, } from '@18f/identity-document-capture'; -import DocumentsStep from '@18f/identity-document-capture/components/documents-step'; +import DocumentsAndSelfieStep from '@18f/identity-document-capture/components/documents-and-selfie-step'; import { composeComponents } from '@18f/identity-compose-components'; import { render } from '../../../support/document-capture'; import { getFixtureFile } from '../../../support/file'; -describe('document-capture/components/documents-step', () => { +describe('document-capture/components/documents-and-selfie-step', () => { it('renders with only front and back inputs by default', () => { - const { getByLabelText, queryByLabelText } = render(); + const { getByLabelText, queryByLabelText } = render( + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + />, + ); const front = getByLabelText('doc_auth.headings.document_capture_front'); const back = getByLabelText('doc_auth.headings.document_capture_back'); @@ -33,8 +43,18 @@ describe('document-capture/components/documents-step', () => { - , + undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> + , , ); const file = await getFixtureFile('doc_auth_images/id-back.jpg'); @@ -52,13 +72,31 @@ describe('document-capture/components/documents-step', () => { it('renders device-specific instructions', () => { let { getByText } = render( - + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> , ); expect(() => getByText('doc_auth.tips.document_capture_id_text4')).to.throw(); - getByText = render().getByText; + getByText = render( + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + />, + ).getByText; expect(() => getByText('doc_auth.tips.document_capture_id_text4')).not.to.throw(); }); @@ -66,8 +104,16 @@ describe('document-capture/components/documents-step', () => { it('renders the hybrid flow warning if the flow is hybrid', () => { const { getByText } = render( - - + + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> , ); @@ -79,8 +125,16 @@ describe('document-capture/components/documents-step', () => { it('does not render the hybrid flow warning if the flow is standard (default)', () => { const { queryByText } = render( - - + + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> , ); @@ -100,7 +154,7 @@ describe('document-capture/components/documents-step', () => { }, }, ], - [DocumentsStep], + [DocumentsAndSelfieStep], ); const { getAllByRole, getByText, getByRole, getByLabelText, queryByLabelText } = render( , @@ -150,7 +204,7 @@ describe('document-capture/components/documents-step', () => { }, }, ], - [DocumentsStep], + [DocumentsAndSelfieStep], ); const { queryByRole, getByRole, getByLabelText } = render(); From c3225b400c597e8bbe3a356fc491952d285adb42 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:33:26 -0400 Subject: [PATCH 14/22] Update configuration link in SDK upgrade documentation (#11111) * docs: internal link yml config path * docs: path can be a slash in front * Add changelog changelog: Internal, Documentation, Link consistently to default application configuration --------- Co-authored-by: Guspan Tanadi <36249910+guspan-tanadi@users.noreply.github.com> --- docs/sdk-upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk-upgrade.md b/docs/sdk-upgrade.md index 7e776ecda3a..b19f0dc23a0 100644 --- a/docs/sdk-upgrade.md +++ b/docs/sdk-upgrade.md @@ -206,7 +206,7 @@ After successful A/B testing clears us to move to a new version of the Acuant SD 1. We want to remove the oldest SDK version from our repository. In the [`/public/acuant/`](/public/acuant/) directory, there should be three versions. We only want to keep the newer two. Delete the directory containing the oldest of the three versions. -2. We also want to update the SDK version in the app's [`config/application.yml.default`](config/application.yml.default) file. This governs the SDK version that will be used by any environment — including one's local dev environment — when no explicit value is set to override it. Modify the file to look something like this: +2. We also want to update the SDK version in the app's [`/config/application.yml.default`](/config/application.yml.default) file. This governs the SDK version that will be used by any environment — including one's local dev environment — when no explicit value is set to override it. Modify the file to look something like this: ```yml idv_acuant_sdk_version_alternate: 11.M.M # previous From 8632b33196be29de14ddff326e03277e55061497 Mon Sep 17 00:00:00 2001 From: A Shukla Date: Mon, 19 Aug 2024 11:21:19 -0500 Subject: [PATCH 15/22] Updated name and references (#11098) * Updated name and refrences * Reverting changes on non front end compnenet * Renamed file resolving PR comment * changelog: internal, code format, change name of class and refrences * Reverting to Unknown error to resolve PR comment * Adding new line to pass lint --- .../document-capture-review-issues.tsx | 4 ++-- .../components/document-capture-warning.tsx | 4 ++-- .../{unknown-error.tsx => general-error.tsx} | 8 +++---- scripts/enforce-typescript-files.mjs | 2 +- ...-error-spec.jsx => general-error-spec.jsx} | 22 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) rename app/javascript/packages/document-capture/components/{unknown-error.tsx => general-error.tsx} (96%) rename spec/javascript/packages/document-capture/components/{unknown-error-spec.jsx => general-error-spec.jsx} (93%) diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index 3ccbe0454cf..1b74567c6a8 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -4,7 +4,7 @@ import { FormStepsButton } from '@18f/identity-form-steps'; import { Cancel } from '@18f/identity-verify-flow'; import { useI18n, HtmlTextWithStrongNoWrap } from '@18f/identity-react-i18n'; import type { FormStepComponentProps } from '@18f/identity-form-steps'; -import UnknownError from './unknown-error'; +import GeneralError from './general-error'; import TipList from './tip-list'; import { SelfieCaptureContext } from '../context'; import { @@ -53,7 +53,7 @@ function DocumentCaptureReviewIssues({ {isSelfieCaptureEnabled && ( )} -
{!!subheading && subheading}
- { +interface GeneralErrorProps extends ComponentProps<'p'> { unknownFieldErrors: FormStepError<{ front: string; back: string }>[]; isFailedDocType: boolean; isFailedSelfie: boolean; @@ -40,7 +40,7 @@ function getError({ unknownFieldErrors }: GetErrorArguments) { return err; } -function UnknownError({ +function GeneralError({ unknownFieldErrors = [], isFailedDocType = false, isFailedSelfie = false, @@ -48,7 +48,7 @@ function UnknownError({ altFailedDocTypeMsg = null, altIsFailedSelfieDontIncludeAttempts = false, hasDismissed, -}: UnknownErrorProps) { +}: GeneralErrorProps) { const { t } = useI18n(); const { getHelpCenterURL } = useContext(MarketingSiteContext); const helpCenterLink = getHelpCenterURL({ @@ -107,4 +107,4 @@ function UnknownError({ return

; } -export default UnknownError; +export default GeneralError; diff --git a/scripts/enforce-typescript-files.mjs b/scripts/enforce-typescript-files.mjs index 6496502e94d..f77f6d396a0 100755 --- a/scripts/enforce-typescript-files.mjs +++ b/scripts/enforce-typescript-files.mjs @@ -55,7 +55,7 @@ const LEGACY_FILE_EXCEPTIONS = [ 'spec/javascript/packages/document-capture/components/submission-spec.jsx', 'spec/javascript/packages/document-capture/components/suspense-error-boundary-spec.jsx', 'spec/javascript/packages/document-capture/components/tip-list-spec.jsx', - 'spec/javascript/packages/document-capture/components/unknown-error-spec.jsx', + 'spec/javascript/packages/document-capture/components/general-error-spec.jsx', 'spec/javascript/packages/document-capture/components/warning-spec.jsx', 'spec/javascript/packages/document-capture/context/acuant-spec.jsx', 'spec/javascript/packages/document-capture/context/device-spec.jsx', diff --git a/spec/javascript/packages/document-capture/components/unknown-error-spec.jsx b/spec/javascript/packages/document-capture/components/general-error-spec.jsx similarity index 93% rename from spec/javascript/packages/document-capture/components/unknown-error-spec.jsx rename to spec/javascript/packages/document-capture/components/general-error-spec.jsx index 5da9baebea5..647442fef3b 100644 --- a/spec/javascript/packages/document-capture/components/unknown-error-spec.jsx +++ b/spec/javascript/packages/document-capture/components/general-error-spec.jsx @@ -1,13 +1,13 @@ -import UnknownError from '@18f/identity-document-capture/components/unknown-error'; +import GeneralError from '@18f/identity-document-capture/components/general-error'; import { toFormEntryError } from '@18f/identity-document-capture/services/upload'; import { within } from '@testing-library/dom'; import { render } from '../../../support/document-capture'; -describe('UnknownError', () => { +describe('GeneralError', () => { context('there is no doc type failure', () => { it('render an empty paragraph when no errors', () => { const { container } = render( - , + , ); expect(container.querySelector('p')).to.be.ok(); }); @@ -15,7 +15,7 @@ describe('UnknownError', () => { context('hasDismissed is true', () => { it('renders error message with errors and a help center link', () => { const { container } = render( - { context('hasDismissed is false', () => { it('renders error message with errors but no link', () => { const { container, queryByRole } = render( - { context('there is a doc type failure', () => { it('renders error message with errors and is a doc type failure', () => { const { container } = render( - { it('renders alternative error message with errors and is a doc type failure', () => { const { container } = render( - { context('there is a selfie quality/liveness failure', () => { it('renders error message with errors', () => { const { container } = render( - { it('renders alternative error message without retry information', () => { const { container } = render( - { context('there is a selfie facematch failure', () => { it('renders error message with errors', () => { const { container } = render( - { it('renders alternative error message without retry information', () => { const { container } = render( - Date: Mon, 19 Aug 2024 12:33:10 -0400 Subject: [PATCH 16/22] LG-13934 Add socure webhook (#11101) Added minimal route and controller for the Socure web hook, and verified that it works from Socure's test bench. Route is '/api/webhooks/socure/event' changelog: Upcoming Features,Adding Socure support,Created a webhook for Socure to invoke during IdV Co-authored-by: Zach Margolis --- app/controllers/socure_webhook_controller.rb | 9 +++++++++ config/routes.rb | 2 ++ spec/controllers/socure_webhook_controller_spec.rb | 12 ++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 app/controllers/socure_webhook_controller.rb create mode 100644 spec/controllers/socure_webhook_controller_spec.rb diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb new file mode 100644 index 00000000000..8e86694c638 --- /dev/null +++ b/app/controllers/socure_webhook_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SocureWebhookController < ApplicationController + skip_before_action :verify_authenticity_token + + def create + render json: { message: 'Got here.' } + end +end diff --git a/config/routes.rb b/config/routes.rb index b311cabff5a..30863b3e145 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,8 @@ post '/api/usps_locations' => 'idv/in_person/public/usps_locations#index' match '/api/usps_locations' => 'idv/in_person/public/usps_locations#options', via: :options + post '/api/webhooks/socure/event' => 'socure_webhook#create' + namespace :api do namespace :internal do get '/sessions' => 'sessions#show' diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb new file mode 100644 index 00000000000..cd2c4c1027a --- /dev/null +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SocureWebhookController do + describe 'POST /api/webhooks/socure/event' do + it 'returns OK' do + post :create + expect(response).to have_http_status(:ok) + end + end +end From 9dac9eb566aaf056fb19d399b478c6a09654e30a Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:02:43 -0400 Subject: [PATCH 17/22] LG-14231: Users can't access accounts after enrollment expires (#11105) * attempt to deactivate profile when expiring ipp enrollments in GetUspsProofingResultsJob * deactivate associated profile after in_person_enrollment expires * Changelog: Upcoming Features, In-Person Proofing, fix bug where user gets locked out of account after in_person_enrollment expires * fix lint error * add spec for deactivate_due_to_ipp_expiration --- app/jobs/get_usps_proofing_results_job.rb | 1 + app/models/profile.rb | 8 +++++++ .../get_usps_proofing_results_job_spec.rb | 23 +++++++++++++++++++ spec/models/profile_spec.rb | 11 +++++++++ 4 files changed, 43 insertions(+) diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index be5e17a31de..058faadb939 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -271,6 +271,7 @@ def handle_expired_status_update(enrollment, response, response_message) status: :expired, status_check_completed_at: Time.zone.now, ) + enrollment.profile.deactivate_due_to_ipp_expiration if fraud_result_pending?(enrollment) analytics(user: enrollment.user).idv_ipp_deactivated_for_never_visiting_post_office( diff --git a/app/models/profile.rb b/app/models/profile.rb index 2b455e41000..0ddb3b925bd 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -196,6 +196,14 @@ def deactivate_due_to_gpo_expiration ) end + def deactivate_due_to_ipp_expiration + update!( + active: false, + deactivation_reason: :verification_cancelled, + in_person_verification_pending_at: nil, + ) + end + def deactivate_for_in_person_verification update!(active: false, in_person_verification_pending_at: Time.zone.now) end diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 9415eacf5ba..77433303c03 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -259,6 +259,12 @@ before do enrollment_records = InPersonEnrollment.where(id: pending_enrollments.map(&:id)) + # Below sets in_person_verification_pending_at + # on the profile associated with each pending enrollment + enrollment_records.each do |enrollment| + profile = enrollment.profile + profile.update(in_person_verification_pending_at: enrollment.created_at) + end allow(InPersonEnrollment).to receive(:needs_usps_status_check). and_return(enrollment_records) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) @@ -877,6 +883,15 @@ ) end + it 'deactivates the associated profile' do + job.perform(Time.zone.now) + + pending_enrollment.reload + expect(pending_enrollment.profile).not_to be_active + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + context 'when the in_person_stop_expiring_enrollments flag is true' do before do allow(IdentityConfig.store).to( @@ -897,6 +912,14 @@ ), ) end + + it 'does not deactivate the profile' do + job.perform(Time.zone.now) + + pending_enrollment.reload + expect(pending_enrollment.profile.in_person_verification_pending_at).to_not be_nil + expect(pending_enrollment.profile.deactivation_reason).to be_nil + end end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 8ebb3d532cd..b90c1bd4dbb 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -1001,6 +1001,17 @@ end end + describe '#deactivate_due_to_ipp_expiration' do + let(:profile) { create(:profile, :in_person_verification_pending) } + it 'updates the profile' do + profile.deactivate_due_to_ipp_expiration + + expect(profile.active).to be false + expect(profile.deactivation_reason).to eq('verification_cancelled') + expect(profile.in_person_verification_pending_at).to be nil + end + end + describe '#deactivate_due_to_gpo_expiration' do let(:profile) { create(:profile, :verify_by_mail_pending, user: user) } From 2f77eb140820086e5da7daa21375aca4fe47edfe Mon Sep 17 00:00:00 2001 From: Shane Chesnutt Date: Mon, 19 Aug 2024 13:57:29 -0400 Subject: [PATCH 18/22] LG-12532 Add 50/50 tests for IPP state id page (#11090) changelog: Internal, Automated Testing, Add 50/50 state integration tests for the state_id step in the ID-IPP/EIPP flow in preperation for removing FSM code. --- .../idv/in_person/state_id/show.html.erb | 2 +- .../steps/in_person/state_id_50_50_spec.rb | 380 ++++++++++++++ .../in_person/state_id_controller_spec.rb | 470 ++++++++++++++++++ .../idv/steps/in_person/state_id_step_spec.rb | 15 +- spec/support/features/in_person_helper.rb | 21 + 5 files changed, 883 insertions(+), 5 deletions(-) create mode 100644 spec/features/idv/steps/in_person/state_id_50_50_spec.rb create mode 100644 spec/features/idv/steps/in_person/state_id_controller_spec.rb diff --git a/app/views/idv/in_person/state_id/show.html.erb b/app/views/idv/in_person/state_id/show.html.erb index 371c75bc08c..c0c13bb3bae 100644 --- a/app/views/idv/in_person/state_id/show.html.erb +++ b/app/views/idv/in_person/state_id/show.html.erb @@ -36,7 +36,7 @@ <%= link_to( MarketingSite.help_center_article_url( category: 'verify-your-identity', - article: 'accepted-state-issued-identification', + article: 'accepted-identification-documents', ), class: 'display-inline', ) do %> diff --git a/spec/features/idv/steps/in_person/state_id_50_50_spec.rb b/spec/features/idv/steps/in_person/state_id_50_50_spec.rb new file mode 100644 index 00000000000..19cfff04a87 --- /dev/null +++ b/spec/features/idv/steps/in_person/state_id_50_50_spec.rb @@ -0,0 +1,380 @@ +require 'rails_helper' + +RSpec.describe 'state id 50/50 state', js: true, allowed_extra_analytics: [:*], + allow_browser_log: true do + include IdvStepHelper + include InPersonHelper + + let(:ipp_service_provider) { create(:service_provider, :active, :in_person_proofing_enabled) } + let(:user) { user_with_2fa } + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(true) + end + + context 'when navigating to state id page from PO search location page' do + context 'when the controller is switched from enabled to disabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + complete_location_step + end + + it 'navigates to the FSM state_id route' do + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + end + end + + context 'when the controller is switched from disabled to enabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + complete_location_step + end + + it 'navigates to the controller state_id route' do + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + end + end + end + + context 'when refreshing the state id page' do + context 'when the controller is switched from enabled to disabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + page.refresh + end + + it 'renders the 404 page' do + expect(page).to have_content( + "The page you were looking for doesn’t exist.\nYou might want to double-check your link" \ + " and try again. (404)", + ) + end + end + + context 'when the controller is switched from disabled to enabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + page.refresh + end + + it 'renders the FSM state_id page' do + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + end + end + end + + context 'when navigating to state id page from verify info page' do + context 'when the controller is switched from enabled to disabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + complete_state_id_controller(user, same_address_as_id: true) + complete_ssn_step(user) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + click_button t('idv.buttons.change_state_id_label') + end + + it 'navigates to the FSM state_id route' do + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + # state id page has fields that are pre-populated + expect(page).to have_field( + t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME, + ) + expect(page).to have_field(t('components.memorable_date.month'), with: '10') + expect(page).to have_field(t('components.memorable_date.day'), with: '6') + expect(page).to have_field(t('components.memorable_date.year'), with: '1938') + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_jurisdiction'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_number'), + with: InPersonHelper::GOOD_STATE_ID_NUMBER, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.identity_doc_address_state'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + end + + context 'when the controller is switched from disabled to enabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + complete_state_id_step(user, same_address_as_id: true) + complete_ssn_step(user) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + click_button t('idv.buttons.change_state_id_label') + end + + it 'navigates to the controller state_id route' do + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + # state id page has fields that are pre-populated + expect(page).to have_field( + t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME, + ) + expect(page).to have_field(t('components.memorable_date.month'), with: '10') + expect(page).to have_field(t('components.memorable_date.day'), with: '6') + expect(page).to have_field(t('components.memorable_date.year'), with: '1938') + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_jurisdiction'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_number'), + with: InPersonHelper::GOOD_STATE_ID_NUMBER, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.identity_doc_address_state'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + end + end + + context 'when updating state id info from the verify info page' do + let(:first_name_update) { 'Natalya' } + + context 'when the controller is switched from enabled to disabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + complete_state_id_controller(user, same_address_as_id: true) + complete_ssn_step(user) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + click_button t('idv.buttons.change_state_id_label') + fill_in t('in_person_proofing.form.state_id.first_name'), with: first_name_update + click_button t('forms.buttons.submit.update') + end + + it 'navigates back to the verify_info page' do + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_text(first_name_update) + end + end + + context 'when the controller is switched from disabled to enabled' do + before do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + begin_in_person_proofing_with_opt_in_ipp_enabled_and_opting_in + complete_prepare_step(user) + complete_location_step + complete_state_id_step(user, same_address_as_id: true) + complete_ssn_step(user) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + click_button t('idv.buttons.change_state_id_label') + fill_in t('in_person_proofing.form.state_id.first_name'), with: first_name_update + click_button t('forms.buttons.submit.update') + end + + it 'navigates back to the verify_info page' do + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_text(first_name_update) + end + end + end + + context 'when navigating to state id page from hybrid PO search location page' do + let(:phone_number) { '415-555-0199' } + + before do + allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true) + allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + @sms_link = config[:link] + impl.call(**config) + end.at_least(1).times + end + + context 'when the controller is switched from enabled to disabled' do + before do + perform_in_browser(:desktop) do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + complete_doc_auth_steps_before_hybrid_handoff_step + click_on t('forms.buttons.continue_remote') + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + mock_doc_auth_fail_face_match_fail + attach_and_submit_images + click_button t('in_person_proofing.body.cta.button') + click_idv_continue + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + complete_location_step + end + end + + it 'navigates to the FSM state_id route' do + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) + end + end + end + + context 'when the controller is switched from disabled to enabled' do + before do + perform_in_browser(:desktop) do + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(false) + visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer }) + sign_in_via_branded_page(user) + complete_doc_auth_steps_before_hybrid_handoff_step + click_on t('forms.buttons.continue_remote') + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + mock_doc_auth_fail_face_match_fail + attach_and_submit_images + click_button t('in_person_proofing.body.cta.button') + click_idv_continue + allow(IdentityConfig.store).to receive( + :in_person_state_id_controller_enabled, + ).and_return(true) + complete_location_step + end + end + + it 'navigates to the controller state_id route' do + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + end + end + end + end +end diff --git a/spec/features/idv/steps/in_person/state_id_controller_spec.rb b/spec/features/idv/steps/in_person/state_id_controller_spec.rb new file mode 100644 index 00000000000..0c96f623ebe --- /dev/null +++ b/spec/features/idv/steps/in_person/state_id_controller_spec.rb @@ -0,0 +1,470 @@ +require 'rails_helper' + +RSpec.describe 'state id controller enabled', js: true, allowed_extra_analytics: [:*] do + include IdvStepHelper + include InPersonHelper + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).and_return(true) + end + + context 'when visiting state id for the first time' do + it 'displays correct heading and button text', allow_browser_log: true do + complete_steps_before_state_id_controller + + expect(page).to have_content(t('forms.buttons.continue')) + expect(page).to have_content( + t( + 'in_person_proofing.headings.state_id_milestone_2', + ).tr(' ', ' '), + ) + end + + it 'allows the user to cancel and start over', allow_browser_log: true do + complete_steps_before_state_id_controller + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.start_over') + expect(page).to have_current_path(idv_welcome_path) + end + + it 'allows the user to cancel and return', allow_browser_log: true do + complete_steps_before_state_id_controller + + expect(page).not_to have_content('forms.buttons.back') + + click_link t('links.cancel') + click_on t('idv.cancel.actions.keep_going') + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + end + + it 'allows user to submit valid inputs on form', allow_browser_log: true do + complete_steps_before_state_id_controller + fill_out_state_id_form_ok(same_address_as_id: true) + click_idv_continue + + expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10) + complete_ssn_step + + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + expect(page).to have_current_path(idv_in_person_verify_info_url) + expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT) + expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE) + end + end + + context 'updating state id page' do + it 'has form fields that are pre-populated', allow_browser_log: true do + complete_steps_before_state_id_controller + + fill_out_state_id_form_ok(same_address_as_id: true) + click_idv_continue + expect(page).to have_current_path(idv_in_person_ssn_url, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_in_person_verify_info_url, wait: 10) + click_button t('idv.buttons.change_state_id_label') + + # state id page has fields that are pre-populated + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_field( + t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME, + ) + expect(page).to have_field(t('components.memorable_date.month'), with: '10') + expect(page).to have_field(t('components.memorable_date.day'), with: '6') + expect(page).to have_field(t('components.memorable_date.year'), with: '1938') + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_jurisdiction'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.state_id_number'), + with: InPersonHelper::GOOD_STATE_ID_NUMBER, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE, + ) + expect(page).to have_field( + t('in_person_proofing.form.state_id.identity_doc_address_state'), + with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + ) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + + context 'same address as id', + allow_browser_log: true do + let(:user) { user_with_2fa } + + before(:each) do + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + complete_prepare_step(user) + complete_location_step(user) + end + + it 'does not update their previous selection of "Yes, + I live at the address on my state-issued ID"' do + complete_state_id_controller(user, same_address_as_id: true) + # skip address step + complete_ssn_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_content(t('headings.verify')) + # expect to see state ID address update on verify twice + expect(page).to have_text('test update address').twice # for state id addr and addr update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # expect "Yes, I live at a different address" is checked" + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + + it 'does not update their previous selection of "No, I live at a different address"' do + complete_state_id_controller(user, same_address_as_id: false) + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + complete_ssn_step(user) + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_content(t('headings.verify')) + # expect to see state ID address update on verify + expect(page).to have_text('test update address').once # only state id address update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_no'), + visible: false, + ) + end + + it 'updates their previous selection from "Yes" TO "No, I live at a different address"' do + complete_state_id_controller(user, same_address_as_id: true) + # skip address step + complete_ssn_step(user) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + # change response to No + choose t('in_person_proofing.form.state_id.same_address_as_id_no') + click_button t('forms.buttons.submit.update') + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # expect to see state ID address update on verify + expect(page).to have_text('test update address').once # only state id address update + # click update state id address + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # check that the "No, I live at a different address" is checked" + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_no'), + visible: false, + ) + end + + it 'updates their previous selection from "No" TO "Yes, + I live at the address on my state-issued ID"' do + complete_state_id_controller(user, same_address_as_id: false) + # expect to be on address page + expect(page).to have_content(t('in_person_proofing.headings.address')) + # complete address step + complete_address_step(user) + complete_ssn_step(user) + # expect to be on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + # change address + fill_in t('in_person_proofing.form.state_id.address1'), with: '' + fill_in t('in_person_proofing.form.state_id.address1'), with: 'test update address' + # change response to Yes + choose t('in_person_proofing.form.state_id.same_address_as_id_yes') + click_button t('forms.buttons.submit.update') + # expect to be back on verify page + expect(page).to have_content(t('headings.verify')) + expect(page).to have_current_path(idv_in_person_verify_info_path) + # expect to see state ID address update on verify twice + expect(page).to have_text('test update address').twice # for state id addr and addr update + # click update state ID button on the verify page + click_button t('idv.buttons.change_state_id_label') + # expect to be on the state ID page + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_checked_field( + t('in_person_proofing.form.state_id.same_address_as_id_yes'), + visible: false, + ) + end + end + end + + context 'Validation' do + it 'validates zip code input', allow_browser_log: true do + complete_steps_before_state_id_controller + + fill_out_state_id_form_ok(same_address_as_id: true) + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '' + fill_in t('in_person_proofing.form.state_id.zipcode'), with: 'invalid input' + expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '') + + # enter valid characters, but invalid length + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123' + click_idv_continue + expect(page).to have_css( + '.usa-error-message', + text: t('idv.errors.pattern_mismatch.zipcode'), + ) + + # enter a valid zip and make sure we can continue + fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123456789' + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: '12345-6789', + ) + click_idv_continue + expect(page).to have_current_path(idv_in_person_ssn_url) + end + + it 'shows error for dob under minimum age', allow_browser_log: true do + complete_steps_before_state_id_controller + + fill_in t('components.memorable_date.month'), with: '1' + fill_in t('components.memorable_date.day'), with: '1' + fill_in t('components.memorable_date.year'), with: Time.zone.now.strftime('%Y') + click_idv_continue + expect(page).to have_content( + t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ) + + year = (Time.zone.now - 13.years).strftime('%Y') + fill_in t('components.memorable_date.year'), with: year + click_idv_continue + expect(page).not_to have_content( + t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ) + end + end + + context 'Transliterable Validation' do + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). + and_return(true) + end + + it 'shows validation errors', + allow_browser_log: true do + complete_steps_before_state_id_controller + + fill_out_state_id_form_ok + fill_in t('in_person_proofing.form.state_id.first_name'), with: 'T0mmy "Lee"' + fill_in t('in_person_proofing.form.state_id.last_name'), with: 'Джейкоб' + fill_in t('in_person_proofing.form.state_id.address1'), with: '#1 $treet' + fill_in t('in_person_proofing.form.state_id.address2'), with: 'Gr@nd Lañe^' + fill_in t('in_person_proofing.form.state_id.city'), with: 'N3w C!ty' + click_idv_continue + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '", 0', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: 'Д, б, е, ж, й, к, о', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '$', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '@, ^', + ), + ) + + expect(page).to have_content( + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: '!, 3', + ), + ) + + fill_in t('in_person_proofing.form.state_id.first_name'), + with: InPersonHelper::GOOD_FIRST_NAME + fill_in t('in_person_proofing.form.state_id.last_name'), + with: InPersonHelper::GOOD_LAST_NAME + fill_in t('in_person_proofing.form.state_id.address1'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1 + fill_in t('in_person_proofing.form.state_id.address2'), + with: InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2 + fill_in t('in_person_proofing.form.state_id.city'), + with: InPersonHelper::GOOD_IDENTITY_DOC_CITY + click_idv_continue + + expect(page).to have_current_path(idv_in_person_address_url, wait: 10) + end + end + + context 'State selection' do + it 'shows address hint when user selects state that has a specific hint', + allow_browser_log: true do + complete_steps_before_state_id_controller + + # state id page + select 'Puerto Rico', + from: t('in_person_proofing.form.state_id.identity_doc_address_state') + + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # change state selection + fill_out_state_id_form_ok(same_address_as_id: true) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).not_to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + + # re-select puerto rico + select 'Puerto Rico', + from: t('in_person_proofing.form.state_id.identity_doc_address_state') + click_idv_continue + + # ssn page + expect(page).to have_current_path(idv_in_person_ssn_url) + complete_ssn_step + + # verify page + expect(page).to have_current_path(idv_in_person_verify_info_path) + expect(page).to have_text('PR') + + # update state ID + click_button t('idv.buttons.change_state_id_label') + + expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address1_hint')) + expect(page).to have_content(I18n.t('in_person_proofing.form.state_id.address2_hint')) + end + + it 'shows id number hint when user selects issuing state that has a specific hint', + allow_browser_log: true do + complete_steps_before_state_id_controller + + # expect default hint to be present + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + + select 'Texas', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_texas_hint')) + expect(page).not_to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + + select 'Florida', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_texas_hint'), + ) + expect(page).not_to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + expect(page).to have_content strip_tags( + t('in_person_proofing.form.state_id.state_id_number_florida_hint_html').gsub( + / /, ' ' + ), + ) + + # select a state without a state specific hint + select 'Ohio', + from: t('in_person_proofing.form.state_id.state_id_jurisdiction') + expect(page).to have_content(t('in_person_proofing.form.state_id.state_id_number_hint')) + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_texas_hint'), + ) + expect(page).not_to have_content( + t('in_person_proofing.form.state_id.state_id_number_florida_hint_html'), + ) + end + end +end diff --git a/spec/features/idv/steps/in_person/state_id_step_spec.rb b/spec/features/idv/steps/in_person/state_id_step_spec.rb index 2acf3f8dfd5..adab4cc66ef 100644 --- a/spec/features/idv/steps/in_person/state_id_step_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_step_spec.rb @@ -6,6 +6,7 @@ before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).and_return(false) end context 'when visiting state id for the first time' do @@ -132,7 +133,7 @@ end it 'does not update their previous selection of "Yes, - I live at the address on my state-issued ID"' do + I live at the address on my state-issued ID"' do complete_state_id_step(user, same_address_as_id: true) # skip address step complete_ssn_step(user) @@ -233,7 +234,7 @@ end it 'updates their previous selection from "No" TO "Yes, - I live at the address on my state-issued ID"' do + I live at the address on my state-issued ID"' do complete_state_id_step(user, same_address_as_id: false) # expect to be on address page expect(page).to have_content(t('in_person_proofing.headings.address')) @@ -282,11 +283,17 @@ # enter valid characters, but invalid length fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123' click_idv_continue - expect(page).to have_css('.usa-error-message', text: t('idv.errors.pattern_mismatch.zipcode')) + expect(page).to have_css( + '.usa-error-message', + text: t('idv.errors.pattern_mismatch.zipcode'), + ) # enter a valid zip and make sure we can continue fill_in t('in_person_proofing.form.state_id.zipcode'), with: '123456789' - expect(page).to have_field(t('in_person_proofing.form.state_id.zipcode'), with: '12345-6789') + expect(page).to have_field( + t('in_person_proofing.form.state_id.zipcode'), + with: '12345-6789', + ) click_idv_continue expect(page).to have_current_path(idv_in_person_ssn_url) end diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 2f130c4a14b..90f82b2ca93 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -131,6 +131,18 @@ def complete_state_id_step(_user = nil, same_address_as_id: true, first_name: GO end end + def complete_state_id_controller(_user = nil, same_address_as_id: true, + first_name: GOOD_FIRST_NAME) + # Wait for page to load before attempting to fill out form + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + fill_out_state_id_form_ok(same_address_as_id: same_address_as_id, first_name:) + click_idv_continue + unless same_address_as_id + expect(page).to have_current_path(idv_in_person_address_path, wait: 10) + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + end + end + def complete_address_step(_user = nil, same_address_as_id: true) fill_out_address_form_ok(same_address_as_id: same_address_as_id) click_idv_continue @@ -155,6 +167,15 @@ def complete_steps_before_state_id_step expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10) end + def complete_steps_before_state_id_controller + sign_in_and_2fa_user + begin_in_person_proofing + complete_prepare_step + complete_location_step + + expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + end + def complete_all_in_person_proofing_steps(user = user_with_2fa, tmx_status = nil, same_address_as_id: true) complete_prepare_step(user) From e26251b2a8271befa26d695bdaee1aa59eedeacd Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:50:47 -0400 Subject: [PATCH 19/22] add regression spec for earlier bug fix [skip changelog] (#11112) --- spec/views/accounts/show.html.erb_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index 3f17dffaadf..d24efb61757 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -111,6 +111,24 @@ end end + context 'when current user has an in_person_enrollment that expired' do + let(:vtr) { ['Pe'] } + let(:sp_name) { 'sinatra-test-app' } + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + before do + # Expire the in_person_enrollment and associated profile + in_person_enrollment = user.in_person_enrollments.first + in_person_enrollment.update!(status: :expired, status_check_completed_at: Time.zone.now) + profile = user.profiles.first + profile.deactivate_due_to_ipp_expiration + end + + it 'renders the idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') + end + end + context 'phone listing and adding' do context 'user has no phone' do let(:user) do From daab1f849ba1a89fe35eeabb518a72941bee4ae1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:01:17 -0400 Subject: [PATCH 20/22] LG-14219: Arrange email as first item in IdV consent screen (#11113) * Add test coverage for expected presenter sort order * LG-14219: Arrange email as first item in IdV consent screen changelog: User-Facing Improvements, Consent Screen, Arrange email as first item in IdV consent screen --- app/presenters/completions_presenter.rb | 4 +- spec/presenters/completions_presenter_spec.rb | 89 +++++++++++-------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/app/presenters/completions_presenter.rb b/app/presenters/completions_presenter.rb index 95fc2e11ee0..52a17f5c60f 100644 --- a/app/presenters/completions_presenter.rb +++ b/app/presenters/completions_presenter.rb @@ -4,11 +4,11 @@ class CompletionsPresenter attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, :completion_context SORTED_IAL2_ATTRIBUTE_MAPPING = [ + [[:email], :email], + [[:all_emails], :all_emails], [%i[given_name family_name], :full_name], [[:address], :address], [[:phone], :phone], - [[:email], :email], - [[:all_emails], :all_emails], [[:birthdate], :birthdate], [[:social_security_number], :social_security_number], [[:x509_subject], :x509_subject], diff --git a/spec/presenters/completions_presenter_spec.rb b/spec/presenters/completions_presenter_spec.rb index ed98928f079..c9bf04488a5 100644 --- a/spec/presenters/completions_presenter_spec.rb +++ b/spec/presenters/completions_presenter_spec.rb @@ -265,16 +265,14 @@ end describe '#pii' do + subject(:pii) { presenter.pii } + context 'ial1' do context 'with a subset of attributes requested' do let(:requested_attributes) { [:email] } it 'properly scopes and resolve attributes' do - expect(presenter.pii).to eq( - { - email: current_user.email, - }, - ) + expect(pii).to eq(email: current_user.email) end end @@ -282,25 +280,28 @@ let(:requested_attributes) { [:email, :all_emails] } it 'only displays all_emails' do - expect(presenter.pii).to eq( - { - all_emails: [current_user.email], - }, - ) + expect(pii).to eq(all_emails: [current_user.email]) end end context 'with all attributes requested' do it 'properly scopes and resolve attributes' do - expect(presenter.pii).to eq( - { - all_emails: [current_user.email], - verified_at: nil, - x509_issuer: nil, - x509_subject: nil, - }, + expect(pii).to eq( + all_emails: [current_user.email], + verified_at: nil, + x509_issuer: nil, + x509_subject: nil, ) end + + it 'builds hash with sorted keys' do + expect(pii.keys).to eq %i[ + all_emails + x509_subject + x509_issuer + verified_at + ] + end end end @@ -311,32 +312,50 @@ let(:requested_attributes) { [:email, :given_name, :phone] } it 'properly scopes and resolve attributes' do - expect(presenter.pii).to eq( - { - email: current_user.email, - full_name: 'Testy Testerson', - phone: '+1 202-212-1000', - }, + expect(pii).to eq( + email: current_user.email, + full_name: 'Testy Testerson', + phone: '+1 202-212-1000', ) end + + it 'builds hash with sorted keys' do + expect(pii.keys).to eq %i[ + email + full_name + phone + ] + end end context 'with all attributes requested' do it 'properly scopes and resolve attributes' do - expect(presenter.pii).to eq( - { - full_name: 'Testy Testerson', - address: '123 main st apt 123 Washington, DC 20405', - phone: '+1 202-212-1000', - all_emails: [current_user.email], - birthdate: 'January 1, 1990', - social_security_number: '900-12-3456', - verified_at: nil, - x509_subject: nil, - x509_issuer: nil, - }, + expect(pii).to eq( + full_name: 'Testy Testerson', + address: '123 main st apt 123 Washington, DC 20405', + phone: '+1 202-212-1000', + all_emails: [current_user.email], + birthdate: 'January 1, 1990', + social_security_number: '900-12-3456', + verified_at: nil, + x509_subject: nil, + x509_issuer: nil, ) end + + it 'builds hash with sorted keys' do + expect(pii.keys).to eq %i[ + all_emails + full_name + address + phone + birthdate + social_security_number + x509_subject + x509_issuer + verified_at + ] + end end end end From 43762a1273a34da410c4fa270abb0677d7a0c50d Mon Sep 17 00:00:00 2001 From: Matt Wagner Date: Mon, 19 Aug 2024 15:56:49 -0400 Subject: [PATCH 21/22] LG-14022 | Merge selected DIVR stats into MKMR (#11072) * LG-14022 | Embed key DIVR metrics in MKMR changelog: Internal, Reporting, Adds some DIVR content to MKMR --------- Co-authored-by: Zach Margolis --- .../reports/monthly_key_metrics_report.rb | 6 + app/mailers/report_mailer.rb | 2 +- app/services/analytics_events.rb | 2 +- app/services/reporting/emailable_report.rb | 1 + .../report_mailer/tables_report.html.erb | 8 +- lib/reporting/identity_verification_report.rb | 28 +++-- lib/reporting/monthly_idv_report.rb | 111 +++++++++++++++++ lib/reporting/proofing_rate_report.rb | 90 +++++++------- .../monthly_key_metrics_report_spec.rb | 7 +- spec/lib/reporting/monthly_idv_report_spec.rb | 112 ++++++++++++++++++ .../reporting/proofing_rate_report_spec.rb | 24 ++-- .../mailers/previews/report_mailer_preview.rb | 1 + 12 files changed, 324 insertions(+), 68 deletions(-) create mode 100644 lib/reporting/monthly_idv_report.rb create mode 100644 spec/lib/reporting/monthly_idv_report_spec.rb diff --git a/app/jobs/reports/monthly_key_metrics_report.rb b/app/jobs/reports/monthly_key_metrics_report.rb index af8a0b92002..d6e82944165 100644 --- a/app/jobs/reports/monthly_key_metrics_report.rb +++ b/app/jobs/reports/monthly_key_metrics_report.rb @@ -2,6 +2,7 @@ require 'csv' require 'reporting/proofing_rate_report' +require 'reporting/monthly_idv_report' module Reports class MonthlyKeyMetricsReport < BaseReport @@ -68,6 +69,7 @@ def reports @reports ||= [ active_users_count_report.active_users_count_emailable_report, total_user_count_report.total_user_count_emailable_report, + monthly_idv_report.monthly_idv_report_emailable_report, proofing_rate_report.proofing_rate_emailable_report, account_deletion_rate_report.account_deletion_emailable_report, account_reuse_report.account_reuse_emailable_report, @@ -113,6 +115,10 @@ def agency_and_sp_report @agency_and_sp_report ||= Reporting::AgencyAndSpReport.new(report_date) end + def monthly_idv_report + @monthly_idv_report ||= Reporting::MonthlyIdvReport.new(end_date: report_date) + end + def upload_to_s3(report_body, report_name: nil) _latest, path = generate_s3_paths(REPORT_NAME, 'csv', subname: report_name, now: report_date) diff --git a/app/mailers/report_mailer.rb b/app/mailers/report_mailer.rb index 1c997e62271..600012a0b36 100644 --- a/app/mailers/report_mailer.rb +++ b/app/mailers/report_mailer.rb @@ -43,7 +43,7 @@ def tables_report( @message = message @reports = reports.map(&:dup).each_with_index do |report, index| - report.title ||= "Table #{index + 1}" + report.title ||= report.subtitle || "Table #{index + 1}" end case attachment_format diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 87b6c76bc44..5d5ca2f8a2f 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3646,7 +3646,7 @@ def idv_phone_confirmation_otp_sent( # @option proofing_components [String,nil] 'threatmetrix_review_status' TMX decision on the user # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. - # When a user attempts to confirm posession of a new phone number during the IDV process + # When a user attempts to confirm possession of a new phone number during the IDV process def idv_phone_confirmation_otp_submitted( success:, errors:, diff --git a/app/services/reporting/emailable_report.rb b/app/services/reporting/emailable_report.rb index 94207c49736..0c152ef0013 100644 --- a/app/services/reporting/emailable_report.rb +++ b/app/services/reporting/emailable_report.rb @@ -16,6 +16,7 @@ module Reporting EmailableReport = Struct.new( :table, :filename, + :subtitle, :title, :float_as_percent, :precision, diff --git a/app/views/report_mailer/tables_report.html.erb b/app/views/report_mailer/tables_report.html.erb index 3645d225f6f..b2c175cb9f2 100644 --- a/app/views/report_mailer/tables_report.html.erb +++ b/app/views/report_mailer/tables_report.html.erb @@ -5,7 +5,13 @@ <%= @message %>
<% @reports.each do |report| %> <% header, *rows = report.table %> -

<%= report.title %>

+ <%# Allow nil title if a subtitle is set %> + <% if report.title && report.title != report.subtitle %> +

<%= report.title %>

+ <% end %> + <% if report.subtitle %> +

<%= report.subtitle %>

+ <% end %> diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb index 1ff5f959aec..24951001dfe 100644 --- a/lib/reporting/identity_verification_report.rb +++ b/lib/reporting/identity_verification_report.rb @@ -85,7 +85,7 @@ def progress? def identity_verification_emailable_report EmailableReport.new( - title: 'Identity Verification Metrics', + subtitle: 'Identity Verification Metrics', table: as_csv, filename: 'identity_verification_metrics', ) @@ -122,10 +122,10 @@ def as_csv csv << ['Successfully Verified - With mailed code', gpo_verification_submitted] csv << ['Successfully Verified - In Person', usps_enrollment_status_updated] csv << ['Successfully Verified - Passed fraud review', fraud_review_passed] - csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', blanket_proofing_rates] - csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', intent_proofing_rates] - csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', actual_proofing_rates] - csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', industry_proofing_rates] + csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', blanket_proofing_rate] + csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', intent_proofing_rate] + csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', actual_proofing_rate] + csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', industry_proofing_rate] end # rubocop:enable Layout/LineLength @@ -152,19 +152,19 @@ def merge(other) ) end - def blanket_proofing_rates + def blanket_proofing_rate successfully_verified_users.to_f / idv_started end - def intent_proofing_rates + def intent_proofing_rate successfully_verified_users.to_f / idv_doc_auth_welcome_submitted end - def actual_proofing_rates + def actual_proofing_rate successfully_verified_users.to_f / idv_doc_auth_image_vendor_submitted end - def industry_proofing_rates + def industry_proofing_rate successfully_verified_users.to_f / (successfully_verified_users + idv_doc_auth_rejected) end @@ -204,6 +204,10 @@ def idv_final_resolution_gpo_in_person_fraud_review data[Results::IDV_FINAL_RESOLUTION_GPO_IN_PERSON_FRAUD_REVIEW].count end + def idv_final_resolution_rate + idv_final_resolution.to_f / idv_started + end + def gpo_verification_submitted @gpo_verification_submitted ||= ( data[Events::GPO_VERIFICATION_SUBMITTED] + @@ -255,6 +259,12 @@ def fraud_review_passed data[Events::FRAUD_REVIEW_PASSED].count end + def verified_user_count + @verified_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where('verified_at <= ?', time_range.end).count + end + end + # rubocop:disable Layout/LineLength # rubocop:disable Metrics/BlockLength # Turns query results into a hash keyed by event name, values are a count of unique users diff --git a/lib/reporting/monthly_idv_report.rb b/lib/reporting/monthly_idv_report.rb new file mode 100644 index 00000000000..614c26d6ff4 --- /dev/null +++ b/lib/reporting/monthly_idv_report.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'csv' +require 'reporting/identity_verification_report' +require 'reporting/unknown_progress_bar' + +module Reporting + class MonthlyIdvReport + attr_reader :end_date + + def initialize(end_date:, verbose: false, progress: false, parallel: false) + @end_date = end_date.in_time_zone('UTC') + @verbose = verbose + @progress = progress + @parallel = parallel + end + + def verbose? + @verbose + end + + def progress? + @progress + end + + def parallel? + @parallel + end + + def monthly_idv_report_emailable_report + EmailableReport.new( + title: 'Proofing Rate Metrics', + subtitle: 'Condensed (NEW)', + float_as_percent: true, + precision: 2, + table: as_csv, + filename: 'condensed_idv', + ) + end + + def as_csv + csv = [] + + csv << ['Metric', *reports.map { |t| t.time_range.begin.strftime('%b %Y') }] + csv << ['IDV started', *reports.map(&:idv_started)] + + csv << ['# of successfully verified users', *reports.map(&:successfully_verified_users)] + csv << ['% IDV started to successfully verified', *reports.map(&:blanket_proofing_rate)] + + csv << ['# of workflow completed', *reports.map(&:idv_final_resolution)] + csv << ['% rate of workflow completed', *reports.map(&:idv_final_resolution_rate)] + + csv << ['# of users verified (total)', *reports.map(&:verified_user_count)] + rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err + [ + ['Error', 'Message'], + [err.class.name, err.message], + ] + end + + def reports + @reports ||= begin + parallel? ? parallel_reports : non_parallel_reports + end + end + + def non_parallel_reports + Reporting::UnknownProgressBar.wrap(show_bar: progress?) do + monthly_subreports.each(&:data) + end + end + + def parallel_reports + threads = monthly_subreports.map do |report| + Thread.new do + report.tap(&:data) + end.tap do |thread| + thread.report_on_exception = false + end + end + + Reporting::UnknownProgressBar.wrap(show_bar: progress?) do + threads.map(&:value) + end + end + + def monthly_subreports + ranges = [ + (end_date - 2.months).all_month, + (end_date - 1.month).all_month, + end_date.all_month, + ] + + ranges.map do |range| + Reporting::IdentityVerificationReport.new( + issuers: nil, # all issuers + time_range: range, + cloudwatch_client: cloudwatch_client, + ) + end + end + + def cloudwatch_client + @cloudwatch_client ||= Reporting::CloudwatchClient.new( + ensure_complete_logs: true, + progress: progress?, + logger: verbose? ? Logger.new(STDERR) : nil, + ) + end + end +end diff --git a/lib/reporting/proofing_rate_report.rb b/lib/reporting/proofing_rate_report.rb index da635c9dd52..ba5cdf44b00 100644 --- a/lib/reporting/proofing_rate_report.rb +++ b/lib/reporting/proofing_rate_report.rb @@ -38,7 +38,7 @@ def parallel? def proofing_rate_emailable_report EmailableReport.new( - title: 'Proofing Rate Metrics', + subtitle: 'Detail', float_as_percent: true, precision: 2, table: as_csv, @@ -62,10 +62,10 @@ def as_csv csv << ['IDV Rejected (Non-Fraud)', *reports.map(&:idv_doc_auth_rejected)] csv << ['IDV Rejected (Fraud)', *reports.map(&:idv_fraud_rejected)] - csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', *reports.map(&:blanket_proofing_rates)] - csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', *reports.map(&:intent_proofing_rates)] - csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', *reports.map(&:actual_proofing_rates)] - csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', *reports.map(&:industry_proofing_rates)] + csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', *reports.map(&:blanket_proofing_rate)] + csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', *reports.map(&:intent_proofing_rate)] + csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', *reports.map(&:actual_proofing_rate)] + csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', *reports.map(&:industry_proofing_rate)] csv rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err @@ -86,42 +86,7 @@ def to_csv def reports @reports ||= begin - sub_reports = [0, *DATE_INTERVALS].each_cons(2).map do |slice_end, slice_start| - time_range = if slice_end.zero? - Range.new( - (end_date - slice_start.days).beginning_of_day, - (end_date - slice_end.days).end_of_day, - ) - else - Range.new( - (end_date - slice_start.days).beginning_of_day, - (end_date - slice_end.days).end_of_day - 1.day, - ) - end - Reporting::IdentityVerificationReport.new( - issuers: nil, # all issuers - time_range: time_range, - cloudwatch_client: cloudwatch_client, - ) - end - - reports = if parallel? - threads = sub_reports.map do |report| - Thread.new do - report.tap(&:data) - end.tap do |thread| - thread.report_on_exception = false - end - end - - Reporting::UnknownProgressBar.wrap(show_bar: progress?) do - threads.map(&:value) - end - else - Reporting::UnknownProgressBar.wrap(show_bar: progress?) do - sub_reports.each(&:data) - end - end + reports = parallel? ? parallel_reports : single_threaded_reports reports.reduce([]) do |acc, report| if acc.empty? @@ -133,6 +98,26 @@ def reports end end + def single_threaded_reports + Reporting::UnknownProgressBar.wrap(show_bar: progress?) do + trailing_days_subreports.each(&:data) + end + end + + def parallel_reports + threads = trailing_days_subreports.map do |report| + Thread.new do + report.tap(&:data) + end.tap do |thread| + thread.report_on_exception = false + end + end + + Reporting::UnknownProgressBar.wrap(show_bar: progress?) do + threads.map(&:value) + end + end + def cloudwatch_client @cloudwatch_client ||= Reporting::CloudwatchClient.new( ensure_complete_logs: true, @@ -141,6 +126,27 @@ def cloudwatch_client wait_duration: wait_duration, ) end + + def trailing_days_subreports + [0, *DATE_INTERVALS].each_cons(2).map do |slice_end, slice_start| + time_range = if slice_end.zero? + Range.new( + (end_date - slice_start.days).beginning_of_day, + (end_date - slice_end.days).end_of_day, + ) + else + Range.new( + (end_date - slice_start.days).beginning_of_day, + (end_date - slice_end.days).end_of_day - 1.day, + ) + end + Reporting::IdentityVerificationReport.new( + issuers: nil, # all issuers + time_range: time_range, + cloudwatch_client: cloudwatch_client, + ) + end + end end end @@ -158,8 +164,8 @@ def cloudwatch_client puts Reporting::ProofingRateReport.new( end_date: end_date, progress: progress, - verbose: verbose, parallel: parallel, + verbose: verbose, ).to_csv end # rubocop:enable Rails/Output diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb index cad5b0ea09b..eebb7131c49 100644 --- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb +++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb @@ -12,6 +12,7 @@ let(:expected_s3_paths) do [ + "#{report_folder}/condensed_idv.csv", "#{report_folder}/account_reuse.csv", "#{report_folder}/account_deletion_rate.csv", "#{report_folder}/total_user_count.csv", @@ -38,9 +39,9 @@ ['Metric', 'Trailing 30d', 'Trailing 60d', 'Trailing 90d'], ] end - let(:mock_proofing_report_data) do + let(:mock_monthly_idv_data) do [ - ['metric', 'num_users', 'percent'], + ['Metric', 'June 2024', 'July 2024', 'August 2024'], ] end @@ -59,6 +60,8 @@ allow(report.proofing_rate_report).to receive(:as_csv). and_return(mock_proofing_rate_data) + allow(report.monthly_idv_report).to receive(:as_csv). + and_return(mock_monthly_idv_data) allow(IdentityConfig.store).to receive(:team_daily_reports_emails). and_return(mock_daily_reports_emails) diff --git a/spec/lib/reporting/monthly_idv_report_spec.rb b/spec/lib/reporting/monthly_idv_report_spec.rb new file mode 100644 index 00000000000..b9433c566ca --- /dev/null +++ b/spec/lib/reporting/monthly_idv_report_spec.rb @@ -0,0 +1,112 @@ +require 'rails_helper' +require 'reporting/monthly_idv_report' + +RSpec.describe Reporting::MonthlyIdvReport do + let(:end_date) { Date.new(2024, 9, 1).in_time_zone('UTC').yesterday.end_of_day } + let(:parallel) { true } + + subject(:idv_report) do + described_class.new(end_date: end_date, parallel: parallel) + end + + describe '#as_csv' do + before do + allow(idv_report).to receive(:reports).and_return( + [ + instance_double( + 'Reporting::IdentityVerificationReport', + time_range: Date.new(2024, 6, 1).all_month, + idv_started: 1111, + successfully_verified_users: 1111, + blanket_proofing_rate: 0.1111, + idv_final_resolution: 1111, + idv_final_resolution_rate: 0.1111, + verified_user_count: 1111, + ), + instance_double( + 'Reporting::IdentityVerificationReport', + time_range: Date.new(2024, 7, 1).all_month, + idv_started: 2222, + successfully_verified_users: 2222, + blanket_proofing_rate: 0.2222, + idv_final_resolution: 2222, + idv_final_resolution_rate: 0.2222, + verified_user_count: 2222, + ), + instance_double( + 'Reporting::IdentityVerificationReport', + time_range: Date.new(2024, 8, 1).all_month, + idv_started: 3333, + successfully_verified_users: 3333, + blanket_proofing_rate: 0.3333, + idv_final_resolution: 3333, + idv_final_resolution_rate: 0.3333, + verified_user_count: 3333, + ), + ], + ) + end + + let(:expected_table) do + [ + ['Metric', 'Jun 2024', 'Jul 2024', 'Aug 2024'], + ['IDV started', 1111, 2222, 3333], + ['# of successfully verified users', 1111, 2222, 3333], + ['% IDV started to successfully verified', 0.1111, 0.2222, + 0.3333], + ['# of workflow completed', 1111, 2222, 3333], + ['% rate of workflow completed', 0.1111, 0.2222, + 0.3333], + ['# of users verified (total)', 1111, 2222, 3333], + ] + end + + it 'reports 3 months of data' do + idv_report.as_csv.zip(expected_table).each do |actual, expected| + expect(actual).to eq(expected) + end + end + + # copied-and-pasted; should this go somewhere else? + context 'when hitting a Cloudwatch rate limit' do + before do + stub_const('CloudwatchClient::DEFAULT_WAIT_DURATION', 0) + + allow(idv_report).to receive(:reports).and_call_original + + Aws.config[:cloudwatchlogs] = { + stub_responses: { + start_query: Aws::CloudWatchLogs::Errors::ThrottlingException.new( + nil, + 'Rate exceeded', + ), + }, + } + end + + it 'renders an error table' do + expect(idv_report.as_csv).to eq( + [ + ['Error', 'Message'], + ['Aws::CloudWatchLogs::Errors::ThrottlingException', 'Rate exceeded'], + ], + ) + end + end + end + + describe '#monthly_subreports' do + let(:june) { Date.new(2024, 6, 1).in_time_zone('UTC').all_month } + let(:july) { Date.new(2024, 7, 1).in_time_zone('UTC').all_month } + let(:august) { Date.new(2024, 8, 1).in_time_zone('UTC').all_month } + + it 'returns IdV reports for the expected months' do + [june, july, august].each do |month| + expect(Reporting::IdentityVerificationReport).to receive(:new). + with(issuers: nil, time_range: month, cloudwatch_client: anything) + end + + subject.monthly_subreports + end + end +end diff --git a/spec/lib/reporting/proofing_rate_report_spec.rb b/spec/lib/reporting/proofing_rate_report_spec.rb index 63e3521c800..daeb261768a 100644 --- a/spec/lib/reporting/proofing_rate_report_spec.rb +++ b/spec/lib/reporting/proofing_rate_report_spec.rb @@ -15,10 +15,10 @@ [ instance_double( 'Reporting::IdentityVerificationReport', - blanket_proofing_rates: 0.25, - intent_proofing_rates: 0.3333333333333333, - actual_proofing_rates: 0.5, - industry_proofing_rates: 0.5, + blanket_proofing_rate: 0.25, + intent_proofing_rate: 0.3333333333333333, + actual_proofing_rate: 0.5, + industry_proofing_rate: 0.5, idv_started: 4, idv_doc_auth_welcome_submitted: 3, idv_doc_auth_image_vendor_submitted: 2, @@ -29,10 +29,10 @@ ), instance_double( 'Reporting::IdentityVerificationReport', - blanket_proofing_rates: 0.4, - intent_proofing_rates: 0.5, - actual_proofing_rates: 0.6666666666666666, - industry_proofing_rates: 0.6666666666666666, + blanket_proofing_rate: 0.4, + intent_proofing_rate: 0.5, + actual_proofing_rate: 0.6666666666666666, + industry_proofing_rate: 0.6666666666666666, idv_started: 5, idv_doc_auth_welcome_submitted: 4, idv_doc_auth_image_vendor_submitted: 3, @@ -43,10 +43,10 @@ ), instance_double( 'Reporting::IdentityVerificationReport', - blanket_proofing_rates: 0.5, - intent_proofing_rates: 0.6, - actual_proofing_rates: 0.75, - industry_proofing_rates: 0.75, + blanket_proofing_rate: 0.5, + intent_proofing_rate: 0.6, + actual_proofing_rate: 0.75, + industry_proofing_rate: 0.75, idv_started: 6, idv_doc_auth_welcome_submitted: 5, idv_doc_auth_image_vendor_submitted: 4, diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb index c4232625b73..a3a831ebcf2 100644 --- a/spec/mailers/previews/report_mailer_preview.rb +++ b/spec/mailers/previews/report_mailer_preview.rb @@ -12,6 +12,7 @@ def monthly_key_metrics_report monthly_key_metrics_report = Reports::MonthlyKeyMetricsReport.new(Time.zone.yesterday) stub_cloudwatch_client(monthly_key_metrics_report.proofing_rate_report) + stub_cloudwatch_client(monthly_key_metrics_report.monthly_idv_report) ReportMailer.tables_report( email: 'test@example.com', From a8977893f15df55fafc14a924f231e3794012bf1 Mon Sep 17 00:00:00 2001 From: A Shukla Date: Tue, 20 Aug 2024 09:29:49 -0500 Subject: [PATCH 22/22] =?UTF-8?q?changelog:=20upcoming=20feature,=20doc=20?= =?UTF-8?q?auth,=20create=20feature=20flag=20for=20future=E2=80=A6=20(#111?= =?UTF-8?q?14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * changelog: upcoming feature, doc auth, create feature flag for future use * Renaming to more specific varaible name --- config/application.yml.default | 1 + lib/identity_config.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/config/application.yml.default b/config/application.yml.default index 47f54bdcde8..bbf2863ccb2 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -95,6 +95,7 @@ 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_selfie_desktop_test_mode: false +doc_auth_separate_pages_enabled: false doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' doc_auth_vendor_randomize: false doc_auth_vendor_randomize_alternate_vendor: '' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 9412d21a65f..52c555d5ffc 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -114,6 +114,7 @@ def self.store 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_selfie_desktop_test_mode, type: :boolean) + config.add(:doc_auth_separate_pages_enabled, type: :boolean) config.add(:doc_auth_supported_country_codes, type: :json) config.add(:doc_auth_vendor, type: :string) config.add(:doc_auth_vendor_randomize, type: :boolean)