diff --git a/Gemfile b/Gemfile index 6b671ec27e0..9cbb44f0a69 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem 'net-sftp' gem 'newrelic_rpm', '~> 8.0' gem 'pg' gem 'phonelib' -gem 'premailer-rails', '>= 1.11.1' +gem 'premailer-rails', '>= 1.12.0' gem 'profanity_filter' gem 'rack', '>= 2.2.3.1' gem 'rack-attack', '>= 6.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 4e7644ded2b..ca2dafb71fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,12 +442,13 @@ GEM google-protobuf (>= 3.19.2) phonelib (0.6.54) pkcs11 (0.3.4) - premailer (1.15.0) + premailer (1.21.0) addressable - css_parser (>= 1.6.0) + css_parser (>= 1.12.0) htmlentities (>= 4.0.0) - premailer-rails (1.11.1) + premailer-rails (1.12.0) actionmailer (>= 3) + net-smtp premailer (~> 1.7, >= 1.7.9) profanity_filter (0.1.1) pry (0.14.1) @@ -778,7 +779,7 @@ DEPENDENCIES pg pg_query phonelib - premailer-rails (>= 1.11.1) + premailer-rails (>= 1.12.0) profanity_filter pry-byebug pry-doc diff --git a/app/controllers/concerns/fraud_review_concern.rb b/app/controllers/concerns/fraud_review_concern.rb index cb59b4743bf..cdc93641f26 100644 --- a/app/controllers/concerns/fraud_review_concern.rb +++ b/app/controllers/concerns/fraud_review_concern.rb @@ -1,6 +1,11 @@ module FraudReviewConcern extend ActiveSupport::Concern + delegate :fraud_check_failed?, + :fraud_review_pending?, + :fraud_rejection?, + to: :fraud_review_checker + def handle_fraud handle_pending_fraud_review handle_fraud_rejection @@ -22,13 +27,7 @@ def redirect_to_fraud_rejection redirect_to idv_not_verified_url end - def fraud_review_pending? - return false unless user_fully_authenticated? - current_user.fraud_review_pending? - end - - def fraud_rejection? - return false unless user_fully_authenticated? - current_user.fraud_rejection? + def fraud_review_checker + @fraud_review_checker ||= FraudReviewChecker.new(current_user) end end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 5e6b3c11ba1..6a791a76cb7 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -45,8 +45,7 @@ def update user_id: current_user.id, threatmetrix_session_id: flow_session[:threatmetrix_session_id], request_ip: request.remote_ip, - double_address_verification: current_user.establishing_in_person_enrollment&. - capture_secondary_id_enabled || false, + double_address_verification: capture_secondary_id_enabled, ) redirect_to after_update_url @@ -54,6 +53,11 @@ def update private + def capture_secondary_id_enabled + current_user.establishing_in_person_enrollment&. + capture_secondary_id_enabled || false + end + def should_use_aamva?(pii) aamva_state?(pii) && !aamva_disallowed_for_service_provider? end diff --git a/app/controllers/idv/gpo_verify_controller.rb b/app/controllers/idv/gpo_verify_controller.rb index 33a1608db9d..6d837c5a031 100644 --- a/app/controllers/idv/gpo_verify_controller.rb +++ b/app/controllers/idv/gpo_verify_controller.rb @@ -111,10 +111,6 @@ def confirm_verification_needed redirect_to account_url end - def fraud_check_failed? - threatmetrix_enabled? && (current_user.fraud_review_pending? || current_user.fraud_rejection?) - end - def threatmetrix_enabled? FeatureManagement.proofing_device_profiling_decisioning_enabled? end diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index a9fd91a3ab0..154ad7c5641 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -13,6 +13,7 @@ class VerifyInfoController < ApplicationController def show @step_indicator_steps = step_indicator_steps + @capture_secondary_id_enabled = capture_secondary_id_enabled analytics.idv_doc_auth_verify_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index dd5c5d4ccd5..2ef957d70c6 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -43,9 +43,13 @@ def extra_view_variables private def confirm_upload_step_complete - return if flow_session['Idv::Steps::UploadStep'] + return if flow_session[:flow_path] == 'hybrid' - redirect_to idv_doc_auth_url + if flow_session[:flow_path] == 'standard' + redirect_to idv_document_capture_url + else + redirect_to idv_doc_auth_url + end end def confirm_document_capture_needed diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index 377b440de40..b622febb64d 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -3,6 +3,7 @@ class PersonalKeyController < ApplicationController include IdvSession include StepIndicatorConcern include SecureHeadersConcern + include FraudReviewConcern before_action :apply_secure_headers_override before_action :confirm_two_factor_authenticated @@ -23,8 +24,8 @@ def update analytics.idv_personal_key_submitted( address_verification_method: address_verification_method, deactivation_reason: idv_session.profile&.deactivation_reason, - fraud_review_pending: idv_session.profile&.fraud_review_pending?, - fraud_rejection: idv_session.profile&.fraud_rejection?, + fraud_review_pending: fraud_review_pending?, + fraud_rejection: fraud_rejection?, ) redirect_to next_step end @@ -38,7 +39,7 @@ def address_verification_method def next_step if in_person_enrollment? idv_in_person_ready_to_verify_url - elsif blocked_by_device_profiling? + elsif fraud_check_failed? idv_please_call_url elsif session[:sp] sign_up_completed_url @@ -91,10 +92,5 @@ def in_person_enrollment? return false unless IdentityConfig.store.in_person_proofing_enabled current_user.pending_in_person_enrollment.present? end - - def blocked_by_device_profiling? - !profile.active && - profile.fraud_review_pending? || profile.fraud_rejection? - end end end diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb index 9667c24058e..ba795fcb6d2 100644 --- a/app/controllers/idv/review_controller.rb +++ b/app/controllers/idv/review_controller.rb @@ -5,6 +5,7 @@ class ReviewController < ApplicationController include IdvStepConcern include StepIndicatorConcern include PhoneConfirmation + include FraudReviewConcern before_action :confirm_verify_info_step_complete before_action :confirm_address_step_complete @@ -18,8 +19,8 @@ def confirm_current_password analytics.idv_review_complete( success: false, - fraud_review_pending: current_user.fraud_review_pending?, - fraud_rejection: current_user.fraud_rejection?, + fraud_review_pending: fraud_review_pending?, + fraud_rejection: fraud_rejection?, ) irs_attempts_api_tracker.idv_password_entered(success: false) diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 1efdd37f611..1383d4d8b77 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -3,8 +3,9 @@ class PhonesController < ApplicationController include PhoneConfirmation include RecaptchaConcern include ReauthenticationRequiredConcern + include MfaSetupConcern - before_action :confirm_two_factor_authenticated + before_action :confirm_user_authenticated_for_2fa_setup before_action :redirect_if_phone_vendor_outage before_action :check_max_phone_numbers_per_account, only: %i[add create] before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled? diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index 18c8d5cd7a0..c8b180ad772 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -21,7 +21,7 @@ def submit if pending_in_person_enrollment? UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(user, pii) pending_profile&.deactivate(:in_person_verification_pending) - elsif fraud_check_failed? && threatmetrix_enabled? + elsif fraud_review_checker.fraud_check_failed? && threatmetrix_enabled? pending_profile&.remove_gpo_deactivation_reason deactivate_for_fraud_review else @@ -37,7 +37,7 @@ def submit enqueued_at: gpo_confirmation_code&.code_sent_at, pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], pending_in_person_enrollment: pending_in_person_enrollment?, - threatmetrix_check_failed: fraud_check_failed?, + threatmetrix_check_failed: fraud_review_checker.fraud_check_failed?, }, ) end @@ -89,8 +89,8 @@ def threatmetrix_enabled? FeatureManagement.proofing_device_profiling_decisioning_enabled? end - def fraud_check_failed? - user.fraud_review_pending? || user.fraud_rejection? + def fraud_review_checker + @fraud_review_checker ||= FraudReviewChecker.new(user) end def activate_profile diff --git a/app/javascript/packages/document-capture/components/location-collection-item.scss b/app/javascript/packages/document-capture/components/location-collection-item.scss index aefbc37d3e9..4b8605bbe2e 100644 --- a/app/javascript/packages/document-capture/components/location-collection-item.scss +++ b/app/javascript/packages/document-capture/components/location-collection-item.scss @@ -12,4 +12,9 @@ margin-top: 1rem; padding-bottom: 1rem; border-color: color('primary-light'); + + &:last-child { + border-bottom-style: none; + padding-bottom: 0; + } } diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index c7030ebae05..147dca8ebf2 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -370,7 +370,7 @@ def handle_successful_status_update(enrollment, response) passed: true, reason: 'Successful status update', ) - enrollment.profile.activate + enrollment.profile.activate_after_passing_in_person enrollment.update( status: :passed, proofed_at: proofed_at, diff --git a/app/models/profile.rb b/app/models/profile.rb index f9c698e45a3..b245e3a2e33 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -71,12 +71,19 @@ def activate_after_passing_review activate end + def activate_after_passing_in_person + update!( + deactivation_reason: nil, + ) + activate + end + def deactivate(reason) update!(active: false, deactivation_reason: reason) end def has_deactivation_reason? - has_fraud_deactivation_reason? || gpo_verification_pending? + deactivation_reason.present? || has_fraud_deactivation_reason? || gpo_verification_pending? end def has_fraud_deactivation_reason? diff --git a/app/models/user.rb b/app/models/user.rb index 3fc8a3ef785..03bab176cfa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -114,10 +114,6 @@ def gpo_verification_pending_profile profiles.where.not(gpo_verification_pending_at: nil).order(created_at: :desc).first end - def fraud_review_eligible? - fraud_review_pending_profile&.fraud_review_pending_at&.after?(30.days.ago) - end - def fraud_review_pending? fraud_review_pending_profile.present? end diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb index dabe125df9d..d7911e42a27 100644 --- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb @@ -95,7 +95,7 @@ def cancel_link def troubleshoot_change_phone_or_method_option if unconfirmed_phone { - url: phone_setup_path, + url: add_phone_path, text: t('two_factor_authentication.phone_verification.troubleshooting.change_number'), } else diff --git a/app/services/fraud_review_checker.rb b/app/services/fraud_review_checker.rb new file mode 100644 index 00000000000..c9941aeb04b --- /dev/null +++ b/app/services/fraud_review_checker.rb @@ -0,0 +1,23 @@ +class FraudReviewChecker + attr_reader :user + + def initialize(user) + @user = user + end + + def fraud_check_failed? + fraud_review_pending? || fraud_rejection? + end + + def fraud_review_pending? + user&.fraud_review_pending_profile.present? + end + + def fraud_rejection? + user&.fraud_rejection_profile.present? + end + + def fraud_review_eligible? + !!user&.fraud_review_pending_profile&.fraud_review_pending_at&.after?(30.days.ago) + end +end diff --git a/app/services/idv/actions/redo_document_capture_action.rb b/app/services/idv/actions/redo_document_capture_action.rb index ae0c395d529..62fb0f4d7a1 100644 --- a/app/services/idv/actions/redo_document_capture_action.rb +++ b/app/services/idv/actions/redo_document_capture_action.rb @@ -7,9 +7,14 @@ def self.analytics_submitted_event def call flow_session['redo_document_capture'] = true - unless flow_session[:skip_upload_step] - mark_step_incomplete(:link_sent) + if flow_session[:skip_upload_step] + redirect_to idv_document_capture_url + else mark_step_incomplete(:upload) + + if !IdentityConfig.store.doc_auth_link_sent_controller_enabled + mark_step_incomplete(:link_sent) + end end end end diff --git a/app/services/idv/steps/in_person/state_id_step.rb b/app/services/idv/steps/in_person/state_id_step.rb index e99943eff03..e978f9d47fb 100644 --- a/app/services/idv/steps/in_person/state_id_step.rb +++ b/app/services/idv/steps/in_person/state_id_step.rb @@ -37,7 +37,7 @@ def call end end - maybe_redirect_to_verify_info if updating_state_id? + maybe_redirect_to_verify_info(flow_session[steps[:address].to_s].blank?) end def extra_view_variables diff --git a/app/services/idv/steps/temp_maybe_redirect_to_verify_info_helper.rb b/app/services/idv/steps/temp_maybe_redirect_to_verify_info_helper.rb index b276c088348..7d4b4d8e319 100644 --- a/app/services/idv/steps/temp_maybe_redirect_to_verify_info_helper.rb +++ b/app/services/idv/steps/temp_maybe_redirect_to_verify_info_helper.rb @@ -1,14 +1,15 @@ +## # This module and calls to it can be removed when the in_person_verify_info_controller_enabled # flag is removed. # - module Idv module Steps module TempMaybeRedirectToVerifyInfoHelper private - def maybe_redirect_to_verify_info + def maybe_redirect_to_verify_info(skip = false) return unless IdentityConfig.store.in_person_verify_info_controller_enabled + return if skip flow_session[:flow_path] = @flow.flow_path redirect_to idv_in_person_verify_info_url end diff --git a/app/services/idv/steps/upload_step.rb b/app/services/idv/steps/upload_step.rb index 009389bd78a..3b454710952 100644 --- a/app/services/idv/steps/upload_step.rb +++ b/app/services/idv/steps/upload_step.rb @@ -75,7 +75,8 @@ def handle_phone_submission failure_reason: failure_reason, ) - if IdentityConfig.store.doc_auth_link_sent_controller_enabled + if !failure_reason && + IdentityConfig.store.doc_auth_link_sent_controller_enabled flow_session[:flow_path] = 'hybrid' redirect_to idv_link_sent_url end @@ -98,7 +99,7 @@ def application def bypass_send_link_steps mark_step_complete(:link_sent) - flow_session[:flow_path] = @flow.flow_path + flow_session[:flow_path] = 'standard' redirect_to idv_document_capture_url form_response(destination: :document_capture) diff --git a/app/views/idv/in_person/verify_info/show.html.erb b/app/views/idv/in_person/verify_info/show.html.erb index fd5c71f4ebb..ef76d2e0c9d 100644 --- a/app/views/idv/in_person/verify_info/show.html.erb +++ b/app/views/idv/in_person/verify_info/show.html.erb @@ -40,8 +40,13 @@ locals: <%= render PageHeadingComponent.new.with_content(t('headings.verify')) %>
-
+
+ <% if @capture_secondary_id_enabled %> +
+ <%= t('headings.state_id') %> +
+ <% end %>
<%= t('idv.form.first_name') %>:
<%= @pii[:first_name] %>
@@ -56,22 +61,55 @@ locals: <%= I18n.l(Date.parse(@pii[:dob]), format: I18n.t('time.formats.event_date')) %>
+ <% if @capture_secondary_id_enabled %> +
+
<%= t('idv.form.issuing_state') %>:
+
<%= @pii[:state_id_jurisdiction] %>
+
+ <% end %>
<%= t('idv.form.id_number') %>:
<%= @pii[:state_id_number] %>
+ <% if @capture_secondary_id_enabled %> +
+
<%= t('idv.form.address1') %>:
+
<%= @pii[:identity_doc_address1] %>
+
+
+
<%= t('idv.form.address2') %>:
+
<%= @pii[:identity_doc_address2].presence %>
+
+
+
<%= t('idv.form.city') %>:
+
<%= @pii[:identity_doc_city] %>
+
+
+
<%= t('idv.form.state') %>:
+
<%= @pii[:identity_doc_address_state] %>
+
+
+
<%= t('idv.form.zipcode') %>:
+
<%= @pii[:identity_doc_zipcode] %>
+
+ <% end %>
<%= button_to( idv_in_person_step_url(step: :redo_state_id), method: :put, - class: 'usa-button usa-button--unstyled', + class: 'usa-button usa-button--unstyled padding-y-1', 'aria-label': t('idv.buttons.change_state_id_label'), ) { t('idv.buttons.change_label') } %>
-
+
+ <% if @capture_secondary_id_enabled %> +
+ <%= t('headings.residential_address') %> +
+ <% end %>
<%= t('idv.form.address1') %>:
<%= @pii[:address1] %>
@@ -97,13 +135,18 @@ locals: <%= button_to( idv_in_person_step_url(step: :redo_address), method: :put, - class: 'usa-button usa-button--unstyled', + class: 'usa-button usa-button--unstyled padding-y-1', 'aria-label': t('idv.buttons.change_address_label'), ) { t('idv.buttons.change_label') } %>
+ <% if @capture_secondary_id_enabled %> +
+ <%= t('headings.ssn') %> +
+ <% end %> <%= t('idv.form.ssn') %>: <%= render( 'shared/masked_text', @@ -121,7 +164,7 @@ locals: <%= button_to( idv_in_person_step_url(step: :redo_ssn), method: :put, - class: 'usa-button usa-button--unstyled', + class: 'usa-button usa-button--unstyled padding-y-1', 'aria-label': t('idv.buttons.change_ssn_label'), ) { t('idv.buttons.change_label') } %>
diff --git a/app/views/idv/link_sent/show.html.erb b/app/views/idv/link_sent/show.html.erb index 98555371610..b4a18d20abc 100644 --- a/app/views/idv/link_sent/show.html.erb +++ b/app/views/idv/link_sent/show.html.erb @@ -57,4 +57,4 @@ <%= javascript_packs_tag_once 'doc-capture-polling' %> <% end %> -<%= render 'idv/shared/back', action: 'cancel_link_sent', class: 'link-sent-back-link', step_url: :idv_doc_auth_url %> +<%= render 'idv/shared/back', action: 'cancel_link_sent', class: 'link-sent-back-link', step_url: :idv_doc_auth_step_url %> diff --git a/app/views/idv/shared/_verify.html.erb b/app/views/idv/shared/_verify.html.erb index 60b55456c20..9e8d59b4805 100644 --- a/app/views/idv/shared/_verify.html.erb +++ b/app/views/idv/shared/_verify.html.erb @@ -120,7 +120,7 @@ locals:
<% end %>
- <%= t('idv.form.ssn') %> : + <%= t('idv.form.ssn') %>: <%= render( 'shared/masked_text', text: SsnFormatter.format(pii[:ssn]), diff --git a/lib/data_pull.rb b/lib/data_pull.rb index 3ee39913917..4a09da6db8e 100644 --- a/lib/data_pull.rb +++ b/lib/data_pull.rb @@ -11,6 +11,7 @@ def initialize(argv:, stdout:, stderr:) Result = Struct.new( :table, # tabular output, rendered as an ASCII table or as CSV + :json, # output that should only be formatted as JSON :subtask, # name of subtask, used for audit logging :uuids, # Array of UUIDs entered or returned, used for audit logging keyword_init: true, @@ -20,6 +21,7 @@ def initialize(argv:, stdout:, stderr:) :include_missing, :format, :show_help, + :requesting_issuers, keyword_init: true, ) do alias_method :include_missing?, :include_missing @@ -31,6 +33,7 @@ def config include_missing: true, format: :table, show_help: false, + requesting_issuers: [], ) end @@ -46,12 +49,16 @@ def run return end - result = subtask_class.new.run(args: argv, include_missing: config.include_missing?) + result = subtask_class.new.run(args: argv, config:) stderr.puts "*Task*: `#{result.subtask}`" stderr.puts "*UUIDs*: #{result.uuids.map { |uuid| "`#{uuid}`" }.join(', ')}" - render_output(result.table) + if result.json + stdout.puts result.json.to_json + else + render_output(result.table) + end end # @param [Array>] rows @@ -91,32 +98,44 @@ def render_output(rows) # @api private # A subtask is a class that has a run method, the type signature should look like: - # +#run(args: Array, include_missing: Boolean) -> Result+ + # +#run(args: Array, config: Config) -> Result+ # @return [Class,nil] def subtask(name) { 'uuid-lookup' => UuidLookup, 'uuid-convert' => UuidConvert, 'email-lookup' => EmailLookup, + 'ig-request' => InspectorGeneralRequest, }[name] end + # rubocop:disable Metrics/BlockLength def option_parser + basename = File.basename($PROGRAM_NAME) + @option_parser ||= OptionParser.new do |opts| opts.banner = <<~EOS - #{$PROGRAM_NAME} [subcommand] [arguments] [options] + #{basename} [subcommand] [arguments] [options] Example usage: - * #{$PROGRAM_NAME} uuid-lookup email1@example.com email2@example.com + * #{basename} uuid-lookup email1@example.com email2@example.com - * #{$PROGRAM_NAME} uuid-convert partner-uuid1 partner-uuid2 + * #{basename} uuid-convert partner-uuid1 partner-uuid2 - * #{$PROGRAM_NAME} email-lookup uuid1 uuid2 + * #{basename} email-lookup uuid1 uuid2 + + * #{basename} ig-request uuid1 uuid2 --requesting-issuer ABC:DEF:GHI Options: EOS + opts.on('-r=ISSUER', '--requesting-issuer=ISSUER', <<-MSG) do |issuer| + requesting issuer (used for ig-request task) + MSG + config.requesting_issuers << issuer + end + opts.on('--help') do config.show_help = true end @@ -140,9 +159,10 @@ def option_parser end end end + # rubocop:enable Metrics/BlockLength class UuidLookup - def run(args:, include_missing:) + def run(args:, config:) emails = args table = [] @@ -155,7 +175,7 @@ def run(args:, include_missing:) if user table << [email, user.uuid] uuids << user.uuid - elsif include_missing + elsif config.include_missing? table << [email, '[NOT FOUND]'] end end @@ -169,7 +189,7 @@ def run(args:, include_missing:) end class UuidConvert - def run(args:, include_missing:) + def run(args:, config:) partner_uuids = args table = [] @@ -180,7 +200,7 @@ def run(args:, include_missing:) table << [identity.uuid, identity.agency.name, identity.user.uuid] end - if include_missing + if config.include_missing? (partner_uuids - identities.map(&:uuid)).each do |missing_uuid| table << [missing_uuid, '[NOT FOUND]', '[NOT FOUND]'] end @@ -195,7 +215,7 @@ def run(args:, include_missing:) end class EmailLookup - def run(args:, include_missing:) + def run(args:, config:) uuids = args users = User.includes(:email_addresses).where(uuid: uuids).order(:uuid) @@ -209,7 +229,7 @@ def run(args:, include_missing:) end end - if include_missing + if config.include_missing? (uuids - users.map(&:uuid)).each do |missing_uuid| table << [missing_uuid, '[NOT FOUND]', nil] end @@ -222,4 +242,25 @@ def run(args:, include_missing:) ) end end + + class InspectorGeneralRequest + def run(args:, config:) + require 'data_requests/deployed' + ActiveRecord::Base.connection.execute('SET statement_timeout = 0') + uuids = args + + users = uuids.map { |uuid| DataRequests::Deployed::LookupUserByUuid.new(uuid).call }.compact + shared_device_users = DataRequests::Deployed::LookupSharedDeviceUsers.new(users).call + + output = shared_device_users.map do |user| + DataRequests::Deployed::CreateUserReport.new(user, config.requesting_issuers).call + end + + Result.new( + subtask: 'ig-request', + uuids: users.map(&:uuid), + json: output, + ) + end + end end diff --git a/lib/tasks/review_profile.rake b/lib/tasks/review_profile.rake index a448b0917b3..9d888000c42 100644 --- a/lib/tasks/review_profile.rake +++ b/lib/tasks/review_profile.rake @@ -26,7 +26,7 @@ namespace :users do next end - if user.fraud_review_eligible? + if FraudReviewChecker.new(user).fraud_review_eligible? profile = user.fraud_review_pending_profile profile.activate_after_passing_review @@ -73,7 +73,7 @@ namespace :users do next end - if user.fraud_review_eligible? + if FraudReviewChecker.new(user).fraud_review_eligible? profile = user.fraud_review_pending_profile profile.reject_for_fraud(notify_user: true) diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 5e085dd1190..c2491123c6d 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -7,8 +7,7 @@ { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', :flow_path => 'hybrid', - :phone_for_mobile_flow => '201-555-1212', - 'Idv::Steps::UploadStep' => true } + :phone_for_mobile_flow => '201-555-1212' } end let(:user) { create(:user) } @@ -79,13 +78,25 @@ ) end - context 'upload step is not complete' do - it 'redirects to idv_doc_auth_url' do - flow_session['Idv::Steps::UploadStep'] = nil + context '#confirm_upload_step_complete' do + context 'no flow_path' do + it 'redirects to idv_doc_auth_url' do + flow_session[:flow_path] = nil - get :show + get :show + + expect(response).to redirect_to(idv_doc_auth_url) + end + end + + context 'flow_path is standard' do + it 'redirects to idv_document_capture_url' do + flow_session[:flow_path] = 'standard' + + get :show - expect(response).to redirect_to(idv_doc_auth_url) + expect(response).to redirect_to(idv_document_capture_url) + end end end diff --git a/spec/decorators/service_provider_session_decorator_spec.rb b/spec/decorators/service_provider_session_decorator_spec.rb index f608686978f..e8c360af671 100644 --- a/spec/decorators/service_provider_session_decorator_spec.rb +++ b/spec/decorators/service_provider_session_decorator_spec.rb @@ -173,7 +173,7 @@ service_provider_request: ServiceProviderRequestProxy.new, ) - expect(subject.sp_logo_url).is_a? String + expect(subject.sp_logo_url).to be_kind_of(String) end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index df00aac3a8d..c93908a522b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -227,7 +227,7 @@ end end - trait :deactivated_fraud_profile do + trait :fraud_review_pending do fully_registered after :build do |user| @@ -241,6 +241,20 @@ end end + trait :fraud_rejection do + fully_registered + + after :build do |user| + create( + :profile, + :fraud_rejection, + :verified, + :with_pii, + user: user, + ) + end + end + trait :deactivated_password_reset_profile do fully_registered diff --git a/spec/features/idv/actions/cancel_link_sent_action_spec.rb b/spec/features/idv/actions/cancel_link_sent_action_spec.rb index 2cf54a9f177..e90d5e499e2 100644 --- a/spec/features/idv/actions/cancel_link_sent_action_spec.rb +++ b/spec/features/idv/actions/cancel_link_sent_action_spec.rb @@ -4,7 +4,11 @@ include IdvStepHelper include DocAuthHelper + let(:new_controller_enabled) { false } + before do + allow(IdentityConfig.store).to receive(:doc_auth_link_sent_controller_enabled). + and_return(new_controller_enabled) sign_in_and_2fa_user complete_doc_auth_steps_before_link_sent_step end @@ -14,4 +18,15 @@ expect(page).to have_current_path(idv_doc_auth_upload_step) end + + context 'new SendLink controller is enabled' do + let(:new_controller_enabled) { true } + + it 'returns to link sent step', :js do + expect(page).to have_current_path(idv_link_sent_path) + click_doc_auth_back_link + + expect(page).to have_current_path(idv_doc_auth_upload_step) + end + end end diff --git a/spec/features/idv/actions/redo_document_capture_action_spec.rb b/spec/features/idv/actions/redo_document_capture_action_spec.rb index bc45ad8e775..3e6db6cf5e9 100644 --- a/spec/features/idv/actions/redo_document_capture_action_spec.rb +++ b/spec/features/idv/actions/redo_document_capture_action_spec.rb @@ -4,8 +4,12 @@ include IdvStepHelper include DocAuthHelper + let(:new_controller_enabled) { false } + context 'when barcode scan returns a warning', allow_browser_log: true do before do + allow(IdentityConfig.store).to receive(:doc_auth_link_sent_controller_enabled). + and_return(new_controller_enabled) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_attention_with_barcode @@ -80,5 +84,25 @@ expect(page).not_to have_css('[role="status"]') end end + + context 'with doc_auth_link_sent_controller_enabled flag enabled', + driver: :headless_chrome_mobile do + let(:new_controller_enabled) { true } + + it 'goes to document capture' do + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + + expect(page).to have_css( + '[role="status"]', + text: t( + 'doc_auth.headings.capture_scan_warning_html', + link: warning_link_text, + ).tr(' ', ' '), + ) + click_link warning_link_text + + expect(current_path).to eq(idv_document_capture_path) + end + end end end diff --git a/spec/features/idv/steps/in_person/verify_info_spec.rb b/spec/features/idv/steps/in_person/verify_info_spec.rb index f7b9ecf03f9..a5a85a4dd22 100644 --- a/spec/features/idv/steps/in_person/verify_info_spec.rb +++ b/spec/features/idv/steps/in_person/verify_info_spec.rb @@ -135,4 +135,66 @@ # phone page expect(page).to have_content(t('titles.idv.phone')) end + + context 'with in person verify info controller enabled ' do + let(:capture_secondary_id_enabled) { true } + let(:enrollment) { InPersonEnrollment.new(capture_secondary_id_enabled:) } + let(:user) { user_with_2fa } + let(:same_address_as_id) { true } + let(:double_address_verification) { true } + + before do + allow(IdentityConfig.store).to receive(:in_person_capture_secondary_id_enabled). + and_return(true) + allow(IdentityConfig.store).to receive(:in_person_verify_info_controller_enabled). + and_return(true) + allow(user).to receive(:establishing_in_person_enrollment). + and_return(enrollment) + end + + it 'displays expected headers & data on /verify_info', + allow_browser_log: true do + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + complete_prepare_step(user) + complete_location_step(user) + complete_state_id_step( + user, same_address_as_id: same_address_as_id, + double_address_verification: double_address_verification + ) + click_idv_continue + complete_ssn_step(user) + + # confirm url is /verify_info + expect(page).to have_current_path(idv_in_person_verify_info_path) + + # verify page + expect(page).to have_content(t('headings.verify')) + + # confirm headers are on template + expect(page).to have_content(t('headings.state_id').tr(' ', ' ')) + expect(page).to have_content(t('headings.residential_address')) + expect(page).to have_content(t('headings.ssn')) + + # confirm data is on template + 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( + "#{I18n.t('idv.form.issuing_state')}: #{Idp::Constants:: + MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION}", + ).once + expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER) + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice + expect(page).to have_text( + Idp::Constants:: + MOCK_IDV_APPLICANT_STATE_ID_ADDRESS[:identity_doc_address_state], + ).twice + + expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice + expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) + end + end end diff --git a/spec/features/idv/threat_metrix_pending_spec.rb b/spec/features/idv/threat_metrix_pending_spec.rb index 43198b5b55a..bafd3f8334a 100644 --- a/spec/features/idv/threat_metrix_pending_spec.rb +++ b/spec/features/idv/threat_metrix_pending_spec.rb @@ -69,6 +69,31 @@ expect(current_path).to eq('/auth/result') end + scenario 'users rejected from fraud review cannot perform idv' do + user = create(:user, :fraud_rejection) + + start_idv_from_sp + sign_in_live_with_2fa(user) + + # User is redirected on IdV sign in + expect(page).to have_content(t('idv.failure.verify.heading')) + expect(page).to have_current_path(idv_not_verified_path) + + visit idv_url + + # User cannot enter IdV flow + expect(page).to have_content(t('idv.failure.verify.heading')) + expect(page).to have_current_path(idv_not_verified_path) + + # User able to sign for IAL1 + set_new_browser_session + visit_idp_from_sp_with_ial1(:oidc) + sign_in_live_with_2fa(user) + click_agree_and_continue + + expect(current_path).to eq('/auth/result') + end + scenario 'users ThreatMetrix Pass, it logs idv_tmx_fraud_check event' do freeze_time do complete_all_idv_steps_with(threatmetrix: 'Pass') diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index e1943073afb..c68c3e12033 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -280,4 +280,20 @@ expect(user.reload.phone_configurations.count).to eq(0) end end + + scenario 'troubleshoot adding a phone number' do + user = create(:user, :fully_registered) + phone = '+1 (225) 278-1234' + + sign_in_and_2fa_user(user) + within('.sidenav') do + click_on t('account.navigation.add_phone_number') + end + + fill_in :new_phone_form_phone, with: phone + click_continue + click_link t('two_factor_authentication.phone_verification.troubleshooting.change_number') + + expect(page).to have_current_path(add_phone_path) + end end diff --git a/spec/lib/data_pull_spec.rb b/spec/lib/data_pull_spec.rb index 4abec8ef71a..3700091f4a1 100644 --- a/spec/lib/data_pull_spec.rb +++ b/spec/lib/data_pull_spec.rb @@ -104,6 +104,29 @@ expect(JSON.parse(stdout.string)).to be_empty end end + + describe 'ig-query task' do + let(:service_provider) { create(:service_provider) } + let(:identity) { IdentityLinker.new(user, service_provider).link_identity } + + let(:argv) do + ['ig-request', identity.uuid, '--requesting-issuer', service_provider.issuer] + end + + it 'runs the data requests report and prints it as JSON' do + data_pull.run + + response = JSON.parse(stdout.string, symbolize_names: true) + expect(response.first.keys).to contain_exactly( + :user_id, + :login_uuid, + :requesting_issuer_uuid, + :email_addresses, + :mfa_configurations, + :user_events, + ) + end + end end describe DataPull::UuidLookup do @@ -114,8 +137,9 @@ let(:args) { [*users.map { |u| u.email_addresses.first.email }, 'missing@example.com'] } let(:include_missing) { true } + let(:config) { DataPull::Config.new(include_missing:) } - subject(:result) { subtask.run(args:, include_missing:) } + subject(:result) { subtask.run(args:, config:) } it 'looks up the UUIDs for the given email addresses', aggregate_failures: true do expect(result.table).to eq( @@ -140,7 +164,8 @@ let(:args) { [*agency_identities.map(&:uuid), 'does-not-exist'] } let(:include_missing) { true } - subject(:result) { subtask.run(args:, include_missing:) } + let(:config) { DataPull::Config.new(include_missing:) } + subject(:result) { subtask.run(args:, config:) } it 'converts the agency agency identities to internal UUIDs', aggregate_failures: true do expect(result.table).to eq( @@ -165,7 +190,8 @@ let(:args) { [user.uuid, 'does-not-exist'] } let(:include_missing) { true } - subject(:result) { subtask.run(args:, include_missing:) } + let(:config) { DataPull::Config.new(include_missing:) } + subject(:result) { subtask.run(args:, config:) } it 'loads email addresses for the user', aggregate_failures: true do expect(result.table).to match( @@ -183,4 +209,33 @@ end end end + + describe DataPull::InspectorGeneralRequest do + subject(:subtask) { DataPull::InspectorGeneralRequest.new } + + describe '#run' do + let(:user) { create(:user) } + let(:service_provider) { create(:service_provider) } + let(:identity) { IdentityLinker.new(user, service_provider).link_identity } + let(:args) { [user.uuid] } + let(:config) { DataPull::Config.new(requesting_issuers: [service_provider.issuer]) } + + subject(:result) { subtask.run(args:, config:) } + + it 'runs the create users report, has a JSON-only response', aggregate_failures: true do + expect(result.table).to be_nil + expect(result.json.first.keys).to contain_exactly( + :user_id, + :login_uuid, + :requesting_issuer_uuid, + :email_addresses, + :mfa_configurations, + :user_events, + ) + + expect(result.subtask).to eq('ig-request') + expect(result.uuids).to eq([user.uuid]) + end + end + end end diff --git a/spec/lib/tasks/review_profile_spec.rb b/spec/lib/tasks/review_profile_spec.rb index 38a79addf9e..b38a99eb953 100644 --- a/spec/lib/tasks/review_profile_spec.rb +++ b/spec/lib/tasks/review_profile_spec.rb @@ -2,7 +2,7 @@ require 'rake' describe 'review_profile' do - let(:user) { create(:user, :deactivated_fraud_profile) } + let(:user) { create(:user, :fraud_review_pending) } let(:uuid) { user.uuid } let(:task_name) { nil } @@ -53,6 +53,16 @@ expect(stdout.string).to include('Error: Could not find user with that UUID') end end + + context 'when the user has cancelled verification' do + it 'does not activate the profile' do + user.profiles.first.update!(gpo_verification_pending_at: user.created_at) + + invoke_task + + expect(user.reload.profiles.first.active).to eq(false) + end + end end describe 'users:review:reject' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e74fe2bca5c..72d4730de15 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -529,47 +529,6 @@ end end - describe '#fraud_review_eligible?' do - context 'when fraud_review_pending_at is nil' do - it 'returns false' do - fraud_review_pending_at = nil - - user = create(:user) - user.profiles.create( - fraud_review_pending_at: fraud_review_pending_at, - ) - - expect(user.fraud_review_eligible?).to be_falsey - end - end - - context 'when verified_at is within 30 days' do - it 'returns true' do - fraud_review_pending_at = 15.days.ago - - user = create(:user) - user.profiles.create( - fraud_review_pending_at: fraud_review_pending_at, - ) - - expect(user.fraud_review_eligible?).to eq true - end - end - - context 'when verified_at is older than 30 days' do - it 'returns false' do - fraud_review_pending_at = 45.days.ago - - user = create(:user) - user.profiles.create( - fraud_review_pending_at: fraud_review_pending_at, - ) - - expect(user.fraud_review_eligible?).to eq false - end - end - end - describe '#fraud_review_pending?' do it 'returns true if fraud review is pending' do user = create(:user) diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb index 3d0b40289b2..1da1ffab4f9 100644 --- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb +++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb @@ -63,7 +63,7 @@ it 'should show an option to change phone number' do expect(presenter.troubleshooting_options).to include( { - url: phone_setup_path, + url: add_phone_path, text: t('two_factor_authentication.phone_verification.troubleshooting.change_number'), }, ) diff --git a/spec/requests/rack_attack_spec.rb b/spec/requests/rack_attack_spec.rb index 3bf0ddf219b..e91280f2d1c 100644 --- a/spec/requests/rack_attack_spec.rb +++ b/spec/requests/rack_attack_spec.rb @@ -38,9 +38,18 @@ end context 'when the request is for an asset' do + let(:asset_url) { '/assets/application.css' } + let(:asset_path) { Rails.root.join('public', asset_url.sub(/^\//, '')) } + + before do + asset_dirname = File.dirname(asset_path) + FileUtils.mkdir_p(asset_dirname) unless File.directory?(asset_dirname) + File.write(asset_path, '') unless File.exist?(asset_path) + end + it 'does not throttle' do (requests_per_ip_limit + 1).times do - get '/assets/application.css', headers: { REMOTE_ADDR: '1.2.3.4' } + get asset_url, headers: { REMOTE_ADDR: '1.2.3.4' } end expect(response.status).to eq(200) @@ -48,8 +57,16 @@ end context 'when the request is for a pack' do + let(:pack_url) { '/packs/js/application.js' } + let(:pack_path) { Rails.root.join('public', pack_url.sub(/^\//, '')) } + + before do + pack_dirname = File.dirname(pack_path) + FileUtils.mkdir_p(pack_dirname) unless File.directory?(pack_dirname) + File.write(pack_path, '') unless File.exist?(pack_path) + end + it 'does not throttle' do - pack_url = Dir['public/packs/js/*'].first.gsub(/^public/, '') (requests_per_ip_limit + 1).times do get pack_url, headers: { REMOTE_ADDR: '1.2.3.4' } end diff --git a/spec/services/fraud_review_check_spec.rb b/spec/services/fraud_review_check_spec.rb new file mode 100644 index 00000000000..07a6fde1f70 --- /dev/null +++ b/spec/services/fraud_review_check_spec.rb @@ -0,0 +1,95 @@ +require 'rails_helper' + +RSpec.describe FraudReviewChecker do + subject { described_class.new(user) } + + describe '#fraud_check_failed?' do + context 'the user is not fraud review pending or rejected' do + let(:user) { create(:user) } + + it { expect(subject.fraud_check_failed?).to eq(false) } + end + + context 'the user is fraud review pending' do + let(:user) { create(:user, :fraud_review_pending) } + + it { expect(subject.fraud_check_failed?).to eq(true) } + end + + context 'the user is fraud review rejected' do + let(:user) { create(:user, :fraud_rejection) } + + it { expect(subject.fraud_check_failed?).to eq(true) } + end + end + + describe '#fraud_review_pending?' do + context 'the user is not fraud review pending or rejected' do + let(:user) { create(:user) } + + it { expect(subject.fraud_review_pending?).to eq(false) } + end + + context 'the user is fraud review pending' do + let(:user) { create(:user, :fraud_review_pending) } + + it { expect(subject.fraud_review_pending?).to eq(true) } + end + + context 'the user is fraud review rejected' do + let(:user) { create(:user, :fraud_rejection) } + + it { expect(subject.fraud_review_pending?).to eq(false) } + end + end + + describe '#fraud_rejection?' do + context 'the user is not fraud review pending or rejected' do + let(:user) { create(:user) } + + it { expect(subject.fraud_rejection?).to eq(false) } + end + + context 'the user is fraud review pending' do + let(:user) { create(:user, :fraud_review_pending) } + + it { expect(subject.fraud_rejection?).to eq(false) } + end + + context 'the user is fraud review rejected' do + let(:user) { create(:user, :fraud_rejection) } + + it { expect(subject.fraud_rejection?).to eq(true) } + end + end + + describe '#fraud_review_eligible?' do + context 'the user is not fraud review pending or rejected' do + let(:user) { create(:user) } + + it { expect(subject.fraud_review_eligible?).to eq(false) } + end + + context 'the user is fraud review pending for less than 30 days' do + let(:user) { create(:user, :fraud_review_pending) } + + it { expect(subject.fraud_review_eligible?).to eq(true) } + end + + context 'the user is fraud review pending for more than 30 days' do + let(:user) do + record = create(:user, :fraud_review_pending) + record.fraud_review_pending_profile.update!(fraud_review_pending_at: 31.days.ago) + record + end + + it { expect(subject.fraud_review_eligible?).to eq(false) } + end + + context 'the user is fraud review rejected' do + let(:user) { create(:user, :fraud_rejection) } + + it { expect(subject.fraud_review_eligible?).to eq(false) } + end + end +end diff --git a/spec/support/sp_auth_helper.rb b/spec/support/sp_auth_helper.rb index 20393659bfb..d5ebf28ed4d 100644 --- a/spec/support/sp_auth_helper.rb +++ b/spec/support/sp_auth_helper.rb @@ -57,7 +57,7 @@ def create_in_person_ial2_account_go_back_to_sp_and_sign_out(sp) # Mark IPP as passed enrollment = user.in_person_enrollments.last expect(enrollment).to_not be_nil - enrollment.profile.activate + enrollment.profile.activate_after_passing_in_person enrollment.update(status: :passed) visit_idp_from_sp_with_ial2(sp) diff --git a/spec/views/shared/_nav_branded.html.erb_spec.rb b/spec/views/shared/_nav_branded.html.erb_spec.rb index c1557ba0442..ff9f034c7d5 100644 --- a/spec/views/shared/_nav_branded.html.erb_spec.rb +++ b/spec/views/shared/_nav_branded.html.erb_spec.rb @@ -72,4 +72,21 @@ expect(rendered).to have_css("img[alt*='No logo no problem']") end end + + context 'service provider has a poorly configured logo' do + before do + sp = build_stubbed(:service_provider, logo: 'abc') + decorated_session = ServiceProviderSessionDecorator.new( + sp:, + view_context:, + sp_session: {}, + service_provider_request: nil, + ) + allow(view).to receive(:decorated_session).and_return(decorated_session) + end + + it 'does not raise an exception' do + expect { render }.not_to raise_exception + end + end end diff --git a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb index 512891e9c5c..8903b9c56b0 100644 --- a/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/otp_verification/show.html.erb_spec.rb @@ -243,7 +243,7 @@ render - expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path) + expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path) end end @@ -258,8 +258,7 @@ ) render - - expect(rendered).to have_link(t('forms.two_factor.try_again'), href: phone_setup_path) + expect(rendered).to have_link(t('forms.two_factor.try_again'), href: add_phone_path) end end @@ -341,7 +340,7 @@ expect(rendered).to have_link( t('two_factor_authentication.phone_verification.troubleshooting.change_number'), - href: phone_setup_path, + href: add_phone_path, ) end end diff --git a/webpack.config.js b/webpack.config.js index 63ff55a4c95..adaf513734c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,7 +12,7 @@ const isLocalhost = host === 'localhost'; const isProductionEnv = env === 'production'; const isTestEnv = env === 'test'; const mode = isProductionEnv ? 'production' : 'development'; -const hashSuffix = isProductionEnv ? '-[chunkhash:8]' : ''; +const hashSuffix = isProductionEnv ? '-[chunkhash:8].digested' : ''; const devServerPort = process.env.WEBPACK_PORT; const devtool = process.env.WEBPACK_DEVTOOL || (isProductionEnv ? 'source-map' : 'eval-source-map');