diff --git a/.reek b/.reek index 7c0a066a100..1faf719ec02 100644 --- a/.reek +++ b/.reek @@ -17,6 +17,7 @@ DuplicateMethodCall: - UserFlowExporter#self.massage_assets - BasicAuthUrl#build - fallback_to_english + - Idv::Proofer#load_vendors! FeatureEnvy: exclude: - ActiveJob::Logging::LogSubscriber#json_for @@ -42,6 +43,7 @@ FeatureEnvy: - UserEncryptedAttributeOverrides#find_with_email - Utf8Sanitizer#event_attributes - Utf8Sanitizer#remote_ip + - Idv::Proofer#validate_vendors InstanceVariableAssumption: exclude: - User @@ -89,6 +91,8 @@ TooManyStatements: - UserFlowExporter#self.massage_assets - UserFlowExporter#self.massage_html - UserFlowExporter#self.run + - Idv::Agent#proof + - Idv::Proofer#configure_vendors TooManyMethods: exclude: - Users::ConfirmationsController diff --git a/Gemfile b/Gemfile index 5c4f6c99b9e..63cfa567f89 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ gem 'pg' gem 'phonelib' gem 'phony_rails' gem 'premailer-rails' -gem 'proofer', github: '18F/identity-proofer-gem', tag: 'v1.1.3' +gem 'proofer', github: '18F/identity-proofer-gem', tag: 'v2.3.0' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' gem 'rack-headers_filter' @@ -85,6 +85,7 @@ group :development, :test do gem 'pry-byebug' gem 'rspec-rails', '~> 3.5.2' gem 'slim_lint' + gem 'strong_migrations' gem 'thin' end @@ -109,6 +110,6 @@ group :test do end group :production do - gem 'aamva', git: 'git@github.com:18F/identity-aamva-api-client-gem', tag: 'v1.0.3' + gem 'aamva', git: 'git@github.com:18F/identity-aamva-api-client-gem', tag: 'v2.1.0' gem 'equifax', git: 'git@github.com:18F/identity-equifax-api-client-gem.git', tag: 'v1.1.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 63a407d0453..8ae312e2fa9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: git@github.com:18F/identity-aamva-api-client-gem - revision: f0e5a89e04955097084bb6c093f05c782c150c53 - tag: v1.0.3 + revision: 32297c9700b5dd9eaf32c3cf59cdf65efd90ca32 + tag: v2.1.0 specs: - aamva (0.1.0) + aamva (2.1.0) dotenv hashie httpi @@ -32,10 +32,10 @@ GIT GIT remote: https://github.com/18F/identity-proofer-gem.git - revision: cdf16d24294f183160b93b8d418b294fe836e66d - tag: v1.1.3 + revision: e5aeee957fd0a054cea826bebb91b25e9a6d5e86 + tag: v2.3.0 specs: - proofer (1.1.3) + proofer (2.3.0) GIT remote: https://github.com/18F/redis-session-store.git @@ -579,6 +579,8 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) stringex (2.8.4) + strong_migrations (0.2.2) + activerecord (>= 3.2.0) sysexits (1.2.0) systemu (2.6.5) temple (0.8.0) @@ -643,7 +645,7 @@ GEM whenever (0.10.0) chronic (>= 0.6.3) xml-simple (1.1.5) - xmldsig (0.6.5) + xmldsig (0.6.6) nokogiri (>= 1.6.8, < 2.0.0) xmlenc (0.6.9) activemodel (>= 3.0.0) @@ -744,6 +746,7 @@ DEPENDENCIES slim-rails slim_lint stringex + strong_migrations thin timecop twilio-ruby diff --git a/README.md b/README.md index 9672ef494d1..08804069a41 100644 --- a/README.md +++ b/README.md @@ -265,10 +265,10 @@ login.gov team for credentials and other values. ### Managing translation files -To help us handle extra newlines and make sure we wrap lines consistently, we have a script called `./script/normalize-yaml` that helps format YAML consistently. After importing translations (or making changes to the *.yml files with strings, run this for the IDP app: +To help us handle extra newlines and make sure we wrap lines consistently, we have a script called `./scripts/normalize-yaml` that helps format YAML consistently. After importing translations (or making changes to the *.yml files with strings, run this for the IDP app: ``` $ make normalize_yaml ``` -[mac-test-passphrase-prompt]: mac-test-passphrase-prompt.png "Mac Test Passphrase Prompt" \ No newline at end of file +[mac-test-passphrase-prompt]: mac-test-passphrase-prompt.png "Mac Test Passphrase Prompt" diff --git a/app/assets/images/sp-logos/usss_pix.png b/app/assets/images/sp-logos/usss_pix.png new file mode 100644 index 00000000000..26581a23ccd Binary files /dev/null and b/app/assets/images/sp-logos/usss_pix.png differ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index edf015031bc..27a4cd2ea96 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,6 +70,11 @@ def default_url_options { locale: locale_url_param, host: Figaro.env.domain_name } end + def sign_out + request.cookie_jar.delete('ahoy_visit') + super + end + private # These attributes show up in New Relic traces for all requests. diff --git a/app/controllers/concerns/saml_idp_logout_concern.rb b/app/controllers/concerns/saml_idp_logout_concern.rb index 3354e6996bc..3d75f590bfb 100644 --- a/app/controllers/concerns/saml_idp_logout_concern.rb +++ b/app/controllers/concerns/saml_idp_logout_concern.rb @@ -49,11 +49,7 @@ def name_id_user def sp_slo_identity @_sp_slo_identity ||= begin - if FeatureManagement.enable_agency_based_uuids? - AgencyIdentityLinker.sp_identity_from_uuid(name_id) - else - Identity.includes(:user).find_by(uuid: name_id) - end + AgencyIdentityLinker.sp_identity_from_uuid(name_id) end end @@ -85,7 +81,6 @@ def prepare_saml_logout_response end def prepare_saml_logout_request - validate_saml_request return if slo_session[:logout_response] # store originating SP's logout response in the user session # for final step in SLO diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 9351664c872..0b0038093a5 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -36,7 +36,8 @@ def profile_or_identity_needs_verification? end def track_authorize_analytics(result) - analytics_attributes = result.to_h.except(:redirect_uri) + analytics_attributes = result.to_h.except(:redirect_uri). + merge(user_fully_authenticated: user_fully_authenticated?) analytics.track_event( Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, analytics_attributes diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 70390920203..de8e4b4f80f 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -11,6 +11,7 @@ class SamlIdpController < ApplicationController include VerifySPAttributesConcern skip_before_action :verify_authenticity_token + before_action :validate_saml_logout_request, only: :logout def auth return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated? @@ -26,7 +27,6 @@ def metadata end def logout - track_logout_event prepare_saml_logout_response_and_request return handle_saml_logout_response if slo.successful_saml_response? @@ -38,6 +38,21 @@ def logout private + def validate_saml_logout_request(raw_saml_request = params[:SAMLRequest]) + request_valid = saml_request_valid?(raw_saml_request) + + track_logout_event(request_valid) + return unless raw_saml_request + + head :bad_request unless request_valid + end + + def saml_request_valid?(saml_request) + return false unless saml_request + decode_request(saml_request) + valid_saml_request? + end + def saml_metadata if SamlCertRotationManager.use_new_secrets_for_request?(request) cert_rotation_saml_metadata @@ -96,11 +111,14 @@ def render_template_for(message, action_url, type) ) end - def track_logout_event + def track_logout_event(saml_request_valid) + saml_request = params[:SAMLRequest] result = { - sp_initiated: params[:SAMLRequest].present?, + sp_initiated: saml_request.present?, oidc: false, } + result[:saml_request_valid] = saml_request_valid + analytics.track_event(Analytics::LOGOUT_INITIATED, result) end end diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index 5f18c713cfe..71ae6bbf3fe 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -53,7 +53,7 @@ def form_params def analytics_properties { context: context, - method: params[:otp_delivery_preference], + multi_factor_auth_method: params[:otp_delivery_preference], confirmation_for_phone_change: confirmation_for_phone_change?, } end diff --git a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb index f074da2535e..8811fcaf4ed 100644 --- a/app/controllers/two_factor_authentication/personal_key_verification_controller.rb +++ b/app/controllers/two_factor_authentication/personal_key_verification_controller.rb @@ -6,7 +6,7 @@ class PersonalKeyVerificationController < ApplicationController def show analytics.track_event( - Analytics::MULTI_FACTOR_AUTH_ENTER_PERSONAL_KEY_VISIT, analytics_properties + Analytics::MULTI_FACTOR_AUTH_ENTER_PERSONAL_KEY_VISIT, context: context ) @personal_key_form = PersonalKeyForm.new(current_user) @@ -66,12 +66,5 @@ def handle_valid_otp redirect_to manage_personal_key_url reset_otp_session_data end - - def analytics_properties - { - context: context, - method: 'personal key', - } - end end end diff --git a/app/controllers/verify/phone_controller.rb b/app/controllers/verify/phone_controller.rb index 970ff7b0fbf..292f2a30768 100644 --- a/app/controllers/verify/phone_controller.rb +++ b/app/controllers/verify/phone_controller.rb @@ -17,7 +17,7 @@ def create analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_FORM, result.to_h) if result.success? - submit_idv_job + Idv::Job.submit(idv_session, [:address]) redirect_to verify_phone_result_url else @view_model = view_model @@ -52,13 +52,6 @@ def phone_confirmation_required? idv_session.user_phone_confirmation != true end - def submit_idv_job - Idv::SubmitIdvJob.new( - idv_session: idv_session, - vendor_params: idv_session.params[:phone] - ).submit_phone_job - end - def step_name :phone end diff --git a/app/controllers/verify/sessions_controller.rb b/app/controllers/verify/sessions_controller.rb index 909d7e4b8e8..985279a1e77 100644 --- a/app/controllers/verify/sessions_controller.rb +++ b/app/controllers/verify/sessions_controller.rb @@ -24,7 +24,7 @@ def create analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, result.to_h) if result.success? - submit_idv_job + Idv::Job.submit(idv_session, %i[resolution state_id]) redirect_to verify_session_result_url else process_failure @@ -50,12 +50,6 @@ def destroy private - def submit_idv_job - Idv::SubmitIdvJob.new( - idv_session: idv_session, vendor_params: idv_session.vendor_params - ).submit_profile_job - end - def confirm_step_needed redirect_to verify_address_url if idv_session.profile_confirmation == true end @@ -107,6 +101,7 @@ def idv_form def initialize_idv_session idv_session.params = profile_params.to_h + idv_session.params[:state_id_jurisdiction] = profile_params[:state] idv_session.applicant = idv_session.vendor_params end diff --git a/app/forms/idv/profile_form.rb b/app/forms/idv/profile_form.rb index b0c22422073..64f171c39fc 100644 --- a/app/forms/idv/profile_form.rb +++ b/app/forms/idv/profile_form.rb @@ -4,7 +4,12 @@ class ProfileForm include FormProfileValidator include FormStateIdValidator - PROFILE_ATTRIBUTES = [:state_id_number, :state_id_type, *Pii::Attributes.members].freeze + PROFILE_ATTRIBUTES = [ + :state_id_number, + :state_id_type, + :state_id_jurisdiction, + *Pii::Attributes.members, + ].freeze attr_reader :user attr_accessor(*PROFILE_ATTRIBUTES) diff --git a/app/forms/openid_connect_logout_form.rb b/app/forms/openid_connect_logout_form.rb index 2a22a83f781..225d412ee3a 100644 --- a/app/forms/openid_connect_logout_form.rb +++ b/app/forms/openid_connect_logout_form.rb @@ -55,11 +55,7 @@ def load_identity def identity_from_payload(payload) uuid = payload[:sub] sp = payload[:aud] - if FeatureManagement.enable_agency_based_uuids? - AgencyIdentityLinker.sp_identity_from_uuid_and_sp(uuid, sp) - else - Identity.where(uuid: uuid, service_provider: sp).first - end + AgencyIdentityLinker.sp_identity_from_uuid_and_sp(uuid, sp) end def build_openid_connect_redirector diff --git a/app/jobs/idv/phone_job.rb b/app/jobs/idv/phone_job.rb deleted file mode 100644 index 101084bdfc3..00000000000 --- a/app/jobs/idv/phone_job.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Idv - class PhoneJob < ProoferJob - def verify_identity_with_vendor - confirmation = agent.submit_phone(vendor_params) - result = extract_result(confirmation) - store_result(result) - end - - private - - def vendor - Figaro.env.phone_proofing_vendor.to_sym - end - end -end diff --git a/app/jobs/idv/profile_job.rb b/app/jobs/idv/profile_job.rb deleted file mode 100644 index bee7e03b7b3..00000000000 --- a/app/jobs/idv/profile_job.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Idv - class ProfileJob < ProoferJob - attr_accessor :resolution, :confirmation - - def verify_identity_with_vendor - self.resolution = resolution_agent.start(vendor_params_hash) - - return build_and_store_result unless resolution.success? - - self.confirmation = confirmation_agent.submit_state_id(state_id_vendor_params) - build_and_store_result - end - - private - - def confirmation_agent - Idv::Agent.new( - vendor: Figaro.env.state_id_proofing_vendor.to_sym, - applicant: applicant - ) - end - - def resolution_agent - Idv::Agent.new( - vendor: Figaro.env.profile_proofing_vendor.to_sym, - applicant: applicant - ) - end - - def result_errors - resolution_errors = resolution.errors - confirmation_errors = confirmation&.errors || {} - resolution_errors.merge(confirmation_errors) - end - - def result_reasons - resolution_reasons = resolution.vendor_resp.reasons - confirmation_reasons = confirmation&.vendor_resp&.reasons || [] - resolution_reasons + confirmation_reasons - end - - def result_successful? - return confirmation.success? if confirmation.present? - resolution.success? - end - - def state_id_vendor_params - vendor_params_hash.merge(state_id_jurisdiction: vendor_params_hash[:state]) - end - - def build_and_store_result - resolution_vendor_resp = resolution.vendor_resp - result = Idv::VendorResult.new( - success: result_successful?, - errors: result_errors, - reasons: result_reasons, - normalized_applicant: resolution_vendor_resp.normalized_applicant - ) - store_result(result) - end - - def vendor_params_hash - vendor_params.with_indifferent_access - end - end -end diff --git a/app/jobs/idv/proofer_job.rb b/app/jobs/idv/proofer_job.rb index b0d6865db85..d2d51c5fd1d 100644 --- a/app/jobs/idv/proofer_job.rb +++ b/app/jobs/idv/proofer_job.rb @@ -2,49 +2,34 @@ module Idv class ProoferJob < ApplicationJob queue_as :idv - attr_reader :result_id, :applicant, :vendor_params + attr_reader :result_id, :applicant, :stages - def perform(result_id:, vendor_params:, applicant_json:) + def perform(result_id:, applicant_json:, stages:) @result_id = result_id - @vendor_params = vendor_params - @applicant = applicant_from_json(applicant_json) - perform_identity_proofing - end - - def verify_identity_with_vendor - raise NotImplementedError, "subclass must implement #{__method__}" + @applicant = from_json(applicant_json) + @stages = from_json(stages).map(&:to_sym) + verify_identity_with_vendor end private - def agent - Idv::Agent.new(applicant: applicant, vendor: vendor) - end - - def applicant_from_json(applicant_json) - applicant_attributes = JSON.parse(applicant_json, symbolize_names: true) - Proofer::Applicant.new(applicant_attributes) + def from_json(applicant_json) + JSON.parse(applicant_json, symbolize_names: true) end - def perform_identity_proofing - verify_identity_with_vendor - rescue StandardError - store_failed_job_result + def verify_identity_with_vendor + agent = Idv::Agent.new(applicant) + result = agent.proof(*stages) + store_result(Idv::VendorResult.new(result.to_h)) + rescue StandardError => error + store_failed_job_result(error) raise end - def extract_result(confirmation) - vendor_resp = confirmation.vendor_resp - - Idv::VendorResult.new( - success: confirmation.success?, - errors: confirmation.errors, - reasons: vendor_resp.reasons + def store_failed_job_result(error) + job_failed_result = Idv::VendorResult.new( + errors: { job_failed: true, message: error.message } ) - end - - def store_failed_job_result - job_failed_result = Idv::VendorResult.new(errors: { job_failed: true }) VendorValidatorResultStorage.new.store(result_id: result_id, result: job_failed_result) end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 9d432473f5b..1f41ae6dee3 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -25,8 +25,4 @@ def account_does_not_exist(email, request_id) @sign_up_email_url = sign_up_email_url(request_id: request_id, locale: locale_url_param) mail(to: email, subject: t('user_mailer.account_does_not_exist.subject')) end - - def reset_password(email) - mail(to: email, subject: 'Please reset your password') - end end diff --git a/app/models/agency_identity.rb b/app/models/agency_identity.rb index fee7a835973..b36d11bf973 100644 --- a/app/models/agency_identity.rb +++ b/app/models/agency_identity.rb @@ -2,8 +2,4 @@ class AgencyIdentity < ApplicationRecord belongs_to :user belongs_to :agency validates :uuid, presence: true - - def agency_enabled? - !FeatureManagement.agencies_with_agency_based_uuids.index(agency_id).nil? - end end diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index e229c6a9114..95ee6b49932 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -21,11 +21,7 @@ def user_info private def uuid_from_sp_identity(identity) - if FeatureManagement.enable_agency_based_uuids? - AgencyIdentityLinker.new(identity).link_identity.uuid - else - identity.uuid - end + AgencyIdentityLinker.new(identity).link_identity.uuid end # rubocop:disable Metrics/AbcSize diff --git a/app/services/agency_identity_linker.rb b/app/services/agency_identity_linker.rb index 78f74147c43..18ac76844a4 100644 --- a/app/services/agency_identity_linker.rb +++ b/app/services/agency_identity_linker.rb @@ -5,14 +5,13 @@ def initialize(sp_identity) end def link_identity - ai = find_or_create_agency_identity - return ai if ai&.agency_enabled? - AgencyIdentity.new(user_id: @sp_identity.user_id, uuid: @sp_identity.uuid) + find_or_create_agency_identity || + AgencyIdentity.new(user_id: @sp_identity.user_id, uuid: @sp_identity.uuid) end def self.sp_identity_from_uuid_and_sp(uuid, service_provider) ai = AgencyIdentity.where(uuid: uuid).first - criteria = if ai&.agency_enabled? + criteria = if ai { user_id: ai.user_id, service_provider: service_provider } else { uuid: uuid, service_provider: service_provider } @@ -31,9 +30,7 @@ def self.sp_identity_from_uuid(uuid) private def find_or_create_agency_identity - ai = agency_identity - return ai if ai - create_agency_identity_for_sp + agency_identity || create_agency_identity_for_sp end def create_agency_identity_for_sp diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index a286c97d87d..5cad70e2a7c 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -53,11 +53,7 @@ def add_bundle(attrs) def uuid_getter_function lambda do |principal| identity = principal.decorate.active_identity_for(service_provider) - if FeatureManagement.enable_agency_based_uuids? - AgencyIdentityLinker.new(identity).link_identity.uuid - else - identity.uuid - end + AgencyIdentityLinker.new(identity).link_identity.uuid end end diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index 6c9a0416a17..afffdd037c9 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -9,7 +9,7 @@ def initialize(user, provider) def link_identity(**extra_attrs) attributes = merged_attributes(extra_attrs) identity.update!(attributes) - AgencyIdentityLinker.new(identity).link_identity if FeatureManagement.enable_agency_based_uuids? + AgencyIdentityLinker.new(identity).link_identity identity end diff --git a/app/services/idv/agent.rb b/app/services/idv/agent.rb index 7a755992443..8199a37b9db 100644 --- a/app/services/idv/agent.rb +++ b/app/services/idv/agent.rb @@ -1,13 +1,34 @@ module Idv class Agent - delegate :vendor, :start, :submit_phone, :submit_state_id, to: :agent + class << self + def proofer_attribute?(key) + Idv::Proofer.attribute?(key) + end + end + + def initialize(applicant) + @applicant = applicant.symbolize_keys! + end - def initialize(vendor:, applicant:) - self.agent = Proofer::Agent.new(applicant: applicant, vendor: vendor, kbv: false) + def proof(*stages) + results = { errors: {}, messages: [], exception: nil, success: false } + + stages.each do |stage| + vendor = Idv::Proofer.get_vendor(stage).new + proofer_result = vendor.proof(@applicant) + results = merge_results(results, proofer_result) + break unless proofer_result.success? + end + + results end private - attr_accessor :agent + def merge_results(results, proofer_result) + results.merge(proofer_result.to_h) do |key, orig, current| + key == :messages ? orig + current : current + end + end end end diff --git a/app/services/idv/job.rb b/app/services/idv/job.rb new file mode 100644 index 00000000000..91e28de29a4 --- /dev/null +++ b/app/services/idv/job.rb @@ -0,0 +1,18 @@ +module Idv + module Job + class << self + def submit(idv_session, stages) + result_id = SecureRandom.uuid + + idv_session.async_result_id = result_id + idv_session.async_result_started_at = Time.zone.now.to_i + + Idv::ProoferJob.perform_later( + result_id: result_id, + applicant_json: idv_session.vendor_params.to_json, + stages: stages.to_json + ) + end + end + end +end diff --git a/app/services/idv/profile_maker.rb b/app/services/idv/profile_maker.rb index d919337d3b3..d469df7f8f8 100644 --- a/app/services/idv/profile_maker.rb +++ b/app/services/idv/profile_maker.rb @@ -3,7 +3,10 @@ class ProfileMaker attr_reader :pii_attributes def initialize(applicant:, user:, normalized_applicant:, phone_confirmed:) - self.pii_attributes = pii_from_applicant(applicant, normalized_applicant) + self.pii_attributes = pii_from_applicant( + OpenStruct.new(applicant), + OpenStruct.new(normalized_applicant) + ) self.user = user self.phone_confirmed = phone_confirmed end diff --git a/app/services/idv/profile_step.rb b/app/services/idv/profile_step.rb index aba09faf599..6045c348a9e 100644 --- a/app/services/idv/profile_step.rb +++ b/app/services/idv/profile_step.rb @@ -31,19 +31,19 @@ def increment_attempts_count def update_idv_session idv_session.profile_confirmation = true - idv_session.normalized_applicant_params = vendor_validator_result.normalized_applicant.to_hash + idv_session.normalized_applicant_params = vendor_result idv_session.resolution_successful = true end def extra_analytics_attributes { idv_attempts_exceeded: attempts_exceeded?, - vendor: { reasons: vendor_reasons }, + vendor: { messages: vendor_validator_result.messages }, } end - def vendor_reasons - vendor_validator_result.reasons + def vendor_result + vendor_validator_result.normalized_applicant&.to_hash end end end diff --git a/app/services/idv/proofer.rb b/app/services/idv/proofer.rb new file mode 100644 index 00000000000..327a64f2be4 --- /dev/null +++ b/app/services/idv/proofer.rb @@ -0,0 +1,92 @@ +module Idv + module Proofer + ATTRIBUTES = %i[ + uuid + first_name last_name middle_name gen + address1 address2 city state zipcode + prev_address1 prev_address2 prev_city prev_state prev_zipcode + ssn dob phone email + ccn mortgage home_equity_line auto_loan + bank_account bank_account_type bank_routing + state_id_number state_id_type state_id_jurisdiction + ].freeze + + STAGES = %i[resolution state_id address].freeze + + class << self + attr_accessor :configuration + + @vendors = {} + + def attribute?(key) + ATTRIBUTES.include?(key&.to_sym) + end + + def init + @vendors = configure_vendors(STAGES, configuration) + end + + def get_vendor(stage) + @vendors[stage] + end + + def configure + self.configuration ||= Configuration.new + yield(configuration) + end + + class Configuration + attr_accessor :mock_fallback, :raise_on_missing_proofers, :vendors + def initialize + @mock_fallback = false + @raise_on_missing_proofers = true + @vendors = [] + end + end + + def configure_vendors(stages, config) + external_vendors = loaded_vendors + available_external_vendors = available_vendors(config.vendors, external_vendors) + require_mock_vendors if config.mock_fallback + mock_vendors = loaded_vendors - external_vendors + + vendors = assign_vendors(stages, available_external_vendors, mock_vendors) + + validate_vendors(stages, vendors) if config.raise_on_missing_proofers + + vendors + end + + private + + def loaded_vendors + ::Proofer::Base.subclasses + end + + def available_vendors(configured_vendors, vendors) + vendors.select { |vendor| configured_vendors.include?(vendor.vendor_name) } + end + + def require_mock_vendors + Dir[Rails.root.join('lib', 'proofer_mocks', '*')].each { |file| require file } + end + + def assign_vendors(stages, external_vendors, mock_vendors) + stages.each_with_object({}) do |stage, vendors| + vendor = stage_vendor(stage, external_vendors) || stage_vendor(stage, mock_vendors) + vendors[stage] = vendor if vendor + end + end + + def stage_vendor(stage, vendors) + vendors.find { |vendor| stage == vendor.supported_stage&.to_sym } + end + + def validate_vendors(stages, vendors) + missing_stages = stages - vendors.keys + return if missing_stages.empty? + raise "No proofer vendor configured for stage(s): #{missing_stages.join(', ')}" + end + end + end +end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 74d567bd796..8e6af73b0dd 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -126,7 +126,7 @@ def session end def applicant_params - params.select { |key, _value| Proofer::Applicant.method_defined?(key) } + params.select { |key, _value| Idv::Agent.proofer_attribute?(key) } end def applicant_params_ascii @@ -135,8 +135,8 @@ def applicant_params_ascii def profile_maker @_profile_maker ||= Idv::ProfileMaker.new( - applicant: Proofer::Applicant.new(applicant_params), - normalized_applicant: Proofer::Applicant.new(normalized_applicant_params), + applicant: applicant_params, + normalized_applicant: normalized_applicant_params, phone_confirmed: vendor_phone_confirmation || false, user: current_user ) diff --git a/app/services/idv/submit_idv_job.rb b/app/services/idv/submit_idv_job.rb deleted file mode 100644 index d686bda4b9a..00000000000 --- a/app/services/idv/submit_idv_job.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Idv - class SubmitIdvJob - def initialize(idv_session:, vendor_params:) - @idv_session = idv_session - @vendor_params = vendor_params - end - - def submit_profile_job - update_idv_session - ProfileJob.perform_later(proofer_job_params) - end - - def submit_phone_job - update_idv_session - PhoneJob.perform_later(proofer_job_params) - end - - private - - attr_reader :idv_session, :vendor_params - - def proofer_job_params - { - result_id: result_id, - vendor_params: vendor_params, - applicant_json: idv_session.applicant.to_json, - } - end - - def result_id - @_result_id ||= SecureRandom.uuid - end - - def update_idv_session - idv_session.async_result_id = result_id - idv_session.async_result_started_at = Time.zone.now.to_i - end - end -end diff --git a/app/services/idv/upcase_vendor_env_vars.rb b/app/services/idv/upcase_vendor_env_vars.rb deleted file mode 100644 index b137b94a019..00000000000 --- a/app/services/idv/upcase_vendor_env_vars.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Idv - class UpcaseVendorEnvVars - def call - available_vendors.each do |vendor| - upcase_env_vars(vendor) - end - end - - private - - def available_vendors - env = Figaro.env - [ - env.profile_proofing_vendor, - env.phone_proofing_vendor, - env.state_id_proofing_vendor, - ] - end - - def upcase_env_vars(vendor) - ENV.keys.grep(/^#{vendor}_/).each do |env_var_name| - ENV[env_var_name.upcase] = ENV[env_var_name] - end - end - end -end diff --git a/app/services/idv/vendor_result.rb b/app/services/idv/vendor_result.rb index f8457614dfb..db26e2d1139 100644 --- a/app/services/idv/vendor_result.rb +++ b/app/services/idv/vendor_result.rb @@ -1,23 +1,24 @@ module Idv class VendorResult - attr_reader :success, :errors, :reasons, :normalized_applicant, :timed_out + attr_reader :success, :errors, :messages, :normalized_applicant, :timed_out, :exception def self.new_from_json(json) parsed = JSON.parse(json, symbolize_names: true) applicant = parsed[:normalized_applicant] - parsed[:normalized_applicant] = Proofer::Applicant.new(applicant) if applicant + parsed[:normalized_applicant] = applicant if applicant new(**parsed) end - def initialize(success: nil, errors: {}, reasons: [], - normalized_applicant: nil, timed_out: nil) + def initialize(success: nil, errors: {}, messages: [], + normalized_applicant: nil, timed_out: nil, exception: nil) @success = success @errors = errors - @reasons = reasons + @messages = messages @normalized_applicant = normalized_applicant @timed_out = timed_out + @exception = exception end def success? diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index 776fdb339cd..fecfbe8faf6 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -22,7 +22,7 @@ def self.help_url URI.join(BASE_URL, locale_segment, 'help').to_s end - def self.help_authenticator_app_url - URI.join(BASE_URL, locale_segment, 'help/signing-in/what-is-an-authenticator-app/').to_s + def self.help_authentication_app_url + URI.join(BASE_URL, locale_segment, 'help/signing-in/what-is-an-authentication-app/').to_s end end diff --git a/app/views/exception_notifier/_data.text.erb b/app/views/exception_notifier/_data.text.erb index 1be593c38dc..711ad8f71fc 100644 --- a/app/views/exception_notifier/_data.text.erb +++ b/app/views/exception_notifier/_data.text.erb @@ -5,7 +5,7 @@ Queue: <%= job['queue'] %> Retry: <%= job['retry'] %> Error class: <%= job['error_class'] %> Error message: <%= job['error_message'] %> -Created at: <%= Time.at(job['created_at']) %> -Enqueued at: <%= Time.at(job['enqueued_at']) %> -Failed at: <%= Time.at(job['failed_at']) %> +Created at: <%= Time.at(job['created_at']) rescue 'unknown' %> +Enqueued at: <%= Time.at(job['enqueued_at']) rescue 'unknown' %> +Failed at: <%= Time.at(job['failed_at']) rescue 'unknown' %> Retry count: <%= job['retry_count'] %> diff --git a/app/views/shared/_footer_lite.html.slim b/app/views/shared/_footer_lite.html.slim index 69f3db61956..1360280af59 100644 --- a/app/views/shared/_footer_lite.html.slim +++ b/app/views/shared/_footer_lite.html.slim @@ -1,4 +1,5 @@ - show_language_dropdown = I18n.available_locales.count > 1 +- sanitized_requested_url = request.query_parameters.slice(:request_id) footer.footer.bg-light-blue.sm-bg-navy - if show_language_dropdown @@ -17,7 +18,7 @@ footer.footer.bg-light-blue.sm-bg-navy - I18n.available_locales.each do |locale| li.border-bottom = link_to t("i18n.locale.#{locale}"), - request.query_parameters.merge(locale: locale), + sanitized_requested_url.merge(locale: locale), class: 'block py-12p px2 text-decoration-none blue fs-13p' .container.py1.px2.lg-px0(class="#{'sm-py0' if show_language_dropdown}") @@ -38,7 +39,7 @@ footer.footer.bg-light-blue.sm-bg-navy - I18n.available_locales.each do |locale| li.border-bottom.border-navy = link_to t("i18n.locale.#{locale}"), - request.query_parameters.merge(locale: locale), + sanitized_requested_url.merge(locale: locale), class: 'block pl-24p py2 text-decoration-none white' = link_to t('links.help'), MarketingSite.help_url, class: 'caps h6 blue sm-white text-decoration-none mr3', target: '_blank' diff --git a/app/views/shared/google_analytics/_page_tracking.html.erb b/app/views/shared/google_analytics/_page_tracking.html.erb index 53df7f0f696..526e3c49f70 100644 --- a/app/views/shared/google_analytics/_page_tracking.html.erb +++ b/app/views/shared/google_analytics/_page_tracking.html.erb @@ -12,5 +12,6 @@ ga('create', analyticsKey, 'auto'); ga('set', 'anonymizeIp', true); ga('set', 'forceSSL', true); + ga('set', 'transport', 'xhr'); ga('send', 'pageview'); <% end %> diff --git a/app/views/user_mailer/reset_password.html.slim b/app/views/user_mailer/reset_password.html.slim deleted file mode 100644 index aebc044cc3b..00000000000 --- a/app/views/user_mailer/reset_password.html.slim +++ /dev/null @@ -1,32 +0,0 @@ -p.lead == "During a login.gov security review, we found that your account \ - password was the same as your email address. For your security, \ - we have disabled your old password and you must now reset your \ - password. Although we did not see any suspicious activity on your \ - account, we disabled your password to be extra careful." - -p.lead == "To continue using your login.gov account, please reset your password \ - using the link below. Please note that login.gov needs your password to \ - be different from your email address." - -table.button.expanded.large.radius - tbody - tr - td - table - tbody - tr - td - center - = link_to t('mailer.reset_password.link_text'), - forgot_password_url, - target: '_blank', class: 'float-center', align: 'center' - td.expander - -p - = link_to forgot_password_url, forgot_password_url, target: '_blank' - -table.spacer - tbody - tr - td.s10 height="10px" - |   diff --git a/app/views/users/totp_setup/new.html.slim b/app/views/users/totp_setup/new.html.slim index 7d8ccdc29db..0cf0000d630 100644 --- a/app/views/users/totp_setup/new.html.slim +++ b/app/views/users/totp_setup/new.html.slim @@ -1,6 +1,6 @@ - title t('titles.totp_setup.new') - help_link = link_to t('links.what_is_totp'), - MarketingSite.help_authenticator_app_url, target: :_blank + MarketingSite.help_authentication_app_url, target: :_blank - btn_cls = 'btn btn-primary p0 w-60p bg-light-blue blue h6 regular border-box center' h1.h3.my0 = t('headings.totp_setup.new') diff --git a/certs/sp/usss_prod.crt b/certs/sp/usss_prod.crt new file mode 100644 index 00000000000..dfe346617e9 --- /dev/null +++ b/certs/sp/usss_prod.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnjCCAoagAwIBAgIQjLwoT+vtBa9Ktbn99+V7sjANBgkqhkiG9w0BAQwFADA9 +MQ0wCwYDVQQKEwRVU1NTMQwwCgYDVQQLEwNVQVQxHjAcBgNVBAMTFXBpeC5zZWNy +ZXRzZXJ2aWNlLmdvdjAeFw0xODA0MjMxODAxMjJaFw0yODEyMzEwNDAwMDBaMD0x +DTALBgNVBAoTBFVTU1MxDDAKBgNVBAsTA1VBVDEeMBwGA1UEAxMVcGl4LnNlY3Jl +dHNlcnZpY2UuZ292MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/ET +i8nRC/v3lkRSrgst6b7NWDpZ7dezjKIv6tjXz96OsovtT49KgI4RSGqgVowLN1j8 +nkhfj8leSHju5P6HkME8//HgZB9LAPyokj7hbUwmOH1wHFVf+W7RvuWCd9dE+WdF +FoysRsuaJmtPbz/9e+37FE/gWpu5ZCLXqDuoskTw13F30DBQDBtckT3VAf5mO+IA +YIkUnj+0RsZtvrmuTyfSitHHHzAVPRcyAv18w84WEcb2Rhu5LQmL8jUmUpCMRw8T +nKJYNnRoLgPL/Rec9swB286WtbTHJ8CAPBhfcr2TBQLGgIAu+z1d+S4zRyW2Ud5e +OJ39RpxojddB6vXrKQIDAQABo4GZMIGWMA8GA1UdEwEB/wQFMAMCAQAwEwYDVR0l +BAwwCgYIKwYBBQUHAwEwbgYDVR0BBGcwZYAQwkVmKJAsLAg6HeZIoZZwM6E/MD0x +DTALBgNVBAoTBFVTU1MxDDAKBgNVBAsTA1VBVDEeMBwGA1UEAxMVcGl4LnNlY3Jl +dHNlcnZpY2UuZ292ghCMvChP6+0Fr0q1uf335XuyMA0GCSqGSIb3DQEBDAUAA4IB +AQBhfLsBJBlzc0G19SfAYd30QmkrHW8cGtGaYdHA5QLahxhXWLx2tCh/RmYRbiOM +FCV8fvutGqqS9xZk5hWrkXTpogHQgPQu2b/emv6bmxR+o2cfxmFkMqP4T/fTAcW3 +JWGX5DGliO7+lnK0lQA68mt7DTSsJC70C6YYJqNAfUwKWsm+t4zH/p/HgcbGQB5k +rhsEiTTXzzVqq5v0nVGkVqp9Ha1ptbC203Mz1t23LU5dlh6HpkeKkmQ2Zlqkx4MV +OKQzsDbN7LPm1WXfApYsNm8rIjCDOtinH437GTG+/531IfmpgOT8glK8s1hV165G +NwbrX52CYX4TR+/I7nVFynOG +-----END CERTIFICATE----- diff --git a/config/agencies.yml b/config/agencies.yml index eb989d62eb2..05e68d9c1c1 100644 --- a/config/agencies.yml +++ b/config/agencies.yml @@ -11,6 +11,8 @@ test: name: 'NGA' 6: name: 'DOT' + 7: + name: 'DHS' development: 1: @@ -25,6 +27,8 @@ development: name: 'NGA' 6: name: 'DOT' + 7: + name: 'USSS' production: 1: @@ -39,3 +43,5 @@ production: name: 'NGA' 6: name: 'DOT' + 7: + name: 'USSS' diff --git a/config/application.rb b/config/application.rb index d326fa1a07d..dcdcb81eed2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -3,11 +3,13 @@ Bundler.require(*Rails.groups) +require_relative '../lib/queue_config.rb' + APP_NAME = 'login.gov'.freeze module Upaya class Application < Rails::Application - config.active_job.queue_adapter = :sidekiq + config.active_job.queue_adapter = Upaya::QueueConfig.choose_queue_adapter config.autoload_paths << Rails.root.join('app', 'mailers', 'concerns') config.time_zone = 'UTC' diff --git a/config/application.yml.example b/config/application.yml.example index 6529c9f3d7d..dd093a003be 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -33,6 +33,10 @@ queue_health_check_dead_interval_seconds: '60' # How often to enqueue simple jobs to make sure the background queues are running queue_health_check_frequency_seconds: '30' +# Configuration to probabilistically select the config.active_job.queue_adapter +# Currently known options are: sidekiq, async, inline +queue_adapter_weights: '{"sidekiq": 1}' + # The number of words in the personal key phrase recovery_code_length: '4' @@ -63,7 +67,6 @@ development: aamva_public_key: '123abc' aamva_private_key: '123abc' aamva_verification_url: 'https://example.org:12345/verification/url' - agencies_with_agency_based_uuids: '1,2,3,4,5' async_job_refresh_interval_seconds: '5' async_job_refresh_max_wait_seconds: '15' attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5) @@ -84,10 +87,10 @@ development: database_pool_worker: '5' database_readonly_password: '' database_readonly_username: '' + database_statement_timeout: '2500' database_timeout: '5000' database_username: '' domain_name: 'localhost:3000' - enable_agency_based_uuids: 'true' enable_identity_verification: 'true' enable_rate_limiting: 'false' enable_test_routes: 'true' @@ -122,8 +125,7 @@ development: otp_valid_for: '10' password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c6942d82f7e00740c7594a095fa8de8db17c05314be7b18a5d6dd5683e73eadf6cc95aa633e5ad9a701edb95192a6a105' password_strength_enabled: 'true' - phone_proofing_vendor: 'mock' - profile_proofing_vendor: 'mock' + proofer_mock_fallback: 'true' rack_mini_profiler: 'off' reauthn_window: '120' redis_url: 'redis://localhost:6379/0' @@ -143,7 +145,6 @@ development: service_timeout: '30' session_encryption_key: '27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120' session_timeout_in_minutes: '15' - state_id_proofing_vendor: 'mock' telephony_disabled: 'true' twilio_numbers: '["9999999999","2222222222"]' twilio_sid: 'sid1' @@ -161,7 +162,6 @@ production: aamva_public_key: # Base64 encoded public key for AAMVA aamva_private_key: # Base64 encoded private key for AAMVA aamva_verification_url: # DLDV Verification URL - agencies_with_agency_based_uuids: '1,2,3,4,5' async_job_refresh_interval_seconds: '5' async_job_refresh_max_wait_seconds: '15' attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5) @@ -175,8 +175,8 @@ production: basic_auth_password: disable_email_sending: 'false' dashboard_api_token: + database_statement_timeout: '2500' domain_name: 'login.gov' - enable_agency_based_uuids: 'true' enable_identity_verification: 'false' enable_rate_limiting: 'true' enable_test_routes: 'false' @@ -213,8 +213,6 @@ production: participate_in_dap: 'false' # pair with google_analytics_key password_pepper: # generate via `rake secret` password_strength_enabled: 'true' - phone_proofing_vendor: 'mock' - profile_proofing_vendor: 'mock' reauthn_window: '120' redis_url: 'redis://redis.login.gov.internal:6379' redis_throttle_url: 'redis://redis.login.gov.internal:6379/1' @@ -232,7 +230,6 @@ production: secret_key_base: # generate via `rake secret` session_encryption_key: # generate via `rake secret` session_timeout_in_minutes: '15' - state_id_proofing_vendor: 'mock' twilio_numbers: # Add JSON encoded array of phone numbers twilio_sid: # Twilio SID twilio_auth_token: # Twilio auth token @@ -248,7 +245,6 @@ test: aamva_public_key: '123abc' aamva_private_key: '123abc' aamva_verification_url: 'https://example.org:12345/verification/url' - agencies_with_agency_based_uuids: '1,2,3,4,5' async_job_refresh_interval_seconds: '1' async_job_refresh_max_wait_seconds: '15' attribute_cost: '800$8$1$' # SCrypt::Engine.calibrate(max_time: 0.01) @@ -268,10 +264,10 @@ test: database_pool_worker: database_readonly_password: '' database_readonly_username: '' + database_statement_timeout: '2500' database_timeout: '5000' database_username: '' dashboard_api_token: '123ABC' - enable_agency_based_uuids: 'true' enable_identity_verification: 'true' enable_rate_limiting: 'true' enable_test_routes: 'true' @@ -304,8 +300,7 @@ test: otp_valid_for: '10' password_pepper: 'f22d4b2cafac9066fe2f4416f5b7a32c6942d82f7e00740c7594a095fa8de8db17c05314be7b18a5d6dd5683e73eadf6cc95aa633e5ad9a701edb95192a6a105' password_strength_enabled: 'false' - phone_proofing_vendor: 'mock' - profile_proofing_vendor: 'mock' + proofer_mock_fallback: 'true' reauthn_window: '120' redis_url: 'redis://localhost:6379/0' redis_throttle_url: 'redis://localhost:6379/1' @@ -323,7 +318,6 @@ test: secret_key_base: 'test_secret_key_base' session_encryption_key: '27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120' session_timeout_in_minutes: '15' - state_id_proofing_vendor: 'mock' twilio_numbers: '["9999999999","2222222222"]' twilio_sid: 'sid1' twilio_auth_token: 'token1' diff --git a/config/database.yml b/config/database.yml index 2466c51ed14..906a3d49d5b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -21,7 +21,7 @@ defaults: &defaults checkout_timeout: 5 reaping_frequency: 10 variables: - statement_timeout: 2500 # ms + statement_timeout: <%= Figaro.env.database_statement_timeout.to_i %> development: <<: *defaults diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index 25a77515846..3b97e468389 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -20,6 +20,7 @@ def track_event(data) def exclude? return if FeatureManagement.enable_load_testing_mode? + return if FeatureManagement.use_dashboard_service_providers? super end diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index 715252292e0..eaf7359ea2a 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -6,8 +6,8 @@ ExceptionNotification.configure do |config| config.add_notifier( :email, - email_prefix: "[#{APP_NAME} EXCEPTION - #{LoginGov::Hostdata.env}] ", - sender_address: %("Exception Notifier" ), + email_prefix: "[#{LoginGov::Hostdata.domain} EXCEPTION - #{LoginGov::Hostdata.env}] ", + sender_address: %("Exception Notifier" ), exception_recipients: EXCEPTION_RECIPIENTS, error_grouping: true, sections: %w[request backtrace session] diff --git a/config/initializers/figaro.rb b/config/initializers/figaro.rb index 3d648127799..79828328993 100644 --- a/config/initializers/figaro.rb +++ b/config/initializers/figaro.rb @@ -3,8 +3,8 @@ Figaro.require_keys( 'attribute_cost', 'attribute_encryption_key', + 'database_statement_timeout', 'domain_name', - 'enable_agency_based_uuids', 'enable_identity_verification', 'enable_rate_limiting', 'enable_test_routes', @@ -29,8 +29,6 @@ 'password_max_attempts', 'password_pepper', 'password_strength_enabled', - 'phone_proofing_vendor', - 'profile_proofing_vendor', 'queue_health_check_dead_interval_seconds', 'queue_health_check_frequency_seconds', 'reauthn_window', @@ -46,7 +44,6 @@ 'service_timeout', 'session_encryption_key', 'session_timeout_in_minutes', - 'state_id_proofing_vendor', 'twilio_numbers', 'twilio_sid', 'twilio_auth_token', diff --git a/config/initializers/proofer.rb b/config/initializers/proofer.rb index 912e0d838dd..5bd72aae308 100644 --- a/config/initializers/proofer.rb +++ b/config/initializers/proofer.rb @@ -1,2 +1,9 @@ -# gems require UPPER case ENV variables so translate -Idv::UpcaseVendorEnvVars.new.call +if FeatureManagement.enable_identity_verification? + Idv::Proofer.configure do |config| + config.mock_fallback = Figaro.env.proofer_mock_fallback == 'true' + config.raise_on_missing_proofers = Figaro.env.proofer_raise_on_missing_proofers == 'false' + config.vendors = JSON.parse(Figaro.env.proofer_vendors || '[]') + end + + Idv::Proofer.init +end diff --git a/config/initializers/safe_migrations.rb b/config/initializers/safe_migrations.rb new file mode 100644 index 00000000000..2be30e06c41 --- /dev/null +++ b/config/initializers/safe_migrations.rb @@ -0,0 +1,5 @@ +require 'deploy/migration_statement_timeout' + +ActiveSupport.on_load(:active_record) do + ActiveRecord::Migration.prepend(Deploy::MigrationStatementTimeout) +end diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index 4cb72d23186..96561e7393d 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -16,9 +16,10 @@ "'self'", '*.newrelic.com', '*.nr-data.net', + '*.google-analytics.com', ], font_src: ["'self'", 'data:'], - img_src: ["'self'", 'data:', '*.google-analytics.com'], + img_src: ["'self'", 'data:'], media_src: ["'self'"], object_src: ["'none'"], script_src: [ diff --git a/config/locales/event_types/en.yml b/config/locales/event_types/en.yml index 069c67f6bf9..b8838140c9b 100644 --- a/config/locales/event_types/en.yml +++ b/config/locales/event_types/en.yml @@ -9,6 +9,7 @@ en: authenticator_enabled: Authenticator app enabled eastern_timestamp: "%{timestamp} (Eastern)" email_changed: Email address changed + password_changed: Password changed phone_changed: Phone number changed phone_confirmed: Phone confirmed usps_mail_sent: Letter sent diff --git a/config/locales/event_types/es.yml b/config/locales/event_types/es.yml index 337608d843e..469ef35c4f8 100644 --- a/config/locales/event_types/es.yml +++ b/config/locales/event_types/es.yml @@ -9,6 +9,7 @@ es: authenticator_enabled: App de autenticación permitido eastern_timestamp: "%{timestamp} (hora del Este)" email_changed: Email cambiado + password_changed: Contraseña cambiada phone_changed: Número de teléfono cambiado phone_confirmed: Teléfono confirmado usps_mail_sent: Carta enviada diff --git a/config/locales/event_types/fr.yml b/config/locales/event_types/fr.yml index ae42f75efce..871b417199a 100644 --- a/config/locales/event_types/fr.yml +++ b/config/locales/event_types/fr.yml @@ -9,6 +9,7 @@ fr: authenticator_enabled: Application d'authentification activée eastern_timestamp: "%{timestamp} (Eastern)" email_changed: Adresse courriel modifiée + password_changed: Mot de passe modifié phone_changed: Numéro de téléphone modifié phone_confirmed: Numéro de téléphone confirmé usps_mail_sent: Lettre envoyée diff --git a/config/locales/forms/en.yml b/config/locales/forms/en.yml index e3ed8257175..a8b1eab00f1 100644 --- a/config/locales/forms/en.yml +++ b/config/locales/forms/en.yml @@ -17,7 +17,7 @@ en: show_hdr: Create a strong password messages: current_address: You should be be able to receive mail at this address. - remember_device: Remember this device for %{duration} days + remember_device: Remember this browser for %{duration} days passwords: edit: buttons: diff --git a/config/locales/forms/es.yml b/config/locales/forms/es.yml index 2a37fbcacd1..c698cc70954 100644 --- a/config/locales/forms/es.yml +++ b/config/locales/forms/es.yml @@ -17,7 +17,7 @@ es: show_hdr: Crear una contraseña segura messages: current_address: Debería poder recibir correo en esta dirección. - remember_device: NOT TRANSLATED YET + remember_device: Recuerde este navegador por %{duration} días passwords: edit: buttons: diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml index a6fbaa9e070..9cdb6c8ee2f 100644 --- a/config/locales/forms/fr.yml +++ b/config/locales/forms/fr.yml @@ -18,7 +18,7 @@ fr: messages: current_address: Vous devriez être en mesure de recevoir du courrier à cette adresse. - remember_device: NOT TRANSLATED YET + remember_device: Enregistrer ce navigateur pendant %{duration} jours passwords: edit: buttons: diff --git a/config/locales/openid_connect/en.yml b/config/locales/openid_connect/en.yml index 8b81ddee839..1215f67e68b 100644 --- a/config/locales/openid_connect/en.yml +++ b/config/locales/openid_connect/en.yml @@ -16,7 +16,8 @@ en: invalid_aud: Invalid audience claim, expected %{url} invalid_authentication: Client must authenticate via PKCE or private_key_jwt, missing either code_challenge or client_assertion - invalid_code: invalid code + invalid_code: is invalid either because it expired, or it doesn't match any + user. Please see our documentation at https://developers.login.gov/oidc/#token invalid_code_verifier: code_verifier did not match code_challenge user_info: errors: diff --git a/config/locales/openid_connect/es.yml b/config/locales/openid_connect/es.yml index 22e1460ea31..da60578524a 100644 --- a/config/locales/openid_connect/es.yml +++ b/config/locales/openid_connect/es.yml @@ -16,7 +16,8 @@ es: invalid_aud: Solicitud de audiencia no válida, esperada %{url} invalid_authentication: El cliente debe autenticarse a través de PKCE o private_key_jwt, faltando code_challenge o client_assertion - invalid_code: código inválido + invalid_code: no es válido porque ha caducado o no coincide con ningún usuario. + Consulte nuestra documentación en https://developers.login.gov/oidc/#token invalid_code_verifier: code_verifier no coincide con code_challenge user_info: errors: diff --git a/config/locales/openid_connect/fr.yml b/config/locales/openid_connect/fr.yml index 8c07af7b1ad..d3ac6b746d5 100644 --- a/config/locales/openid_connect/fr.yml +++ b/config/locales/openid_connect/fr.yml @@ -16,7 +16,9 @@ fr: invalid_aud: Affirmation liée à l'auditoire non valide, attendu %{url} invalid_authentication: Le client doit s'authentifier par PKCE ou private_key_jwt, code_challenge ou client_assertion manquant - invalid_code: code non valide + invalid_code: est non valide soit parce qu'il est périmé, soit parce qu'il + ne correspond à aucun utilisateur. Veuillez consulter notre documentation + à https://developers.login.gov/oidc/#token invalid_code_verifier: code_verifier ne correspondait pas à code_challenge user_info: errors: diff --git a/config/service_providers.yml b/config/service_providers.yml index 8e69d9127a9..327f18d49a8 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -598,3 +598,18 @@ production: - 'https://symphony.nga-geoworks.com/guacamole/#/' - 'https://symphony.nga-geoworks.com/secured' restrict_to_deploy_env: 'prod' + + # Secret Service PIX + 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:usss:pix': + agency_id: 7 + friendly_name: 'PIX' + agency: 'USSS' + logo: 'usss_pix.png' + acs_url: 'https://pix.secretservice.gov/auth' + sp_initiated_login_url: 'https://pix.secretservice.gov/logingov' + return_to_sp_url: 'https://pix.secretservice.gov/' + block_encryption: 'aes256-cbc' + cert: 'usss_prod' + attribute_bundle: + - email + restrict_to_deploy_env: 'prod' diff --git a/deploy/migrate b/deploy/migrate new file mode 100755 index 00000000000..c2d2b17e5f3 --- /dev/null +++ b/deploy/migrate @@ -0,0 +1,24 @@ +#!/bin/bash + +# This script is called by identity-devops cookbooks as part of the deployment +# process. It runs any pending migrations. + +set -euo pipefail + +echo "deploy/migrate starting" +echo "HOME: ${HOME-}" +cd "$(dirname "$0")/.." + +set -x + +id +which bundle + +export RAILS_ENV=production +export MIGRATION_STATEMENT_TIMEOUT=60000 + +bundle exec rake db:create db:migrate db:seed --trace + +set +x + +echo "deploy/migrate finished" diff --git a/lib/config_validator.rb b/lib/config_validator.rb index 3779fe740b7..ebe369d9f23 100644 --- a/lib/config_validator.rb +++ b/lib/config_validator.rb @@ -1,14 +1,8 @@ class ConfigValidator ENV_PREFIX = Figaro::Application::FIGARO_ENV_PREFIX - NON_EMPTY_KEYS = %w[ - phone_proofing_vendor - profile_proofing_vendor - state_id_proofing_vendor - ].freeze def validate(env = ENV) validate_boolean_keys(env) - validate_non_empty_keys(env) end private @@ -32,10 +26,6 @@ def candidate_key?(env, key) env.include?(key) and env.include?(ENV_PREFIX + key) end - def empty_keys_warning(empty_keys) - 'These configs are required and were empty: ' + empty_keys.join(', ') - end - def keys_with_bad_boolean_values(env, keys) # Configuration settings for boolean values need to be "true/false" # and not "yes/no". @@ -48,10 +38,4 @@ def validate_boolean_keys(env) return unless bad_keys.any? raise boolean_warning(bad_keys).tr("\n", ' ') end - - def validate_non_empty_keys(env) - empty_keys = NON_EMPTY_KEYS - env.keys - return if empty_keys.empty? - raise empty_keys_warning(empty_keys) - end end diff --git a/lib/deploy/migration_statement_timeout.rb b/lib/deploy/migration_statement_timeout.rb new file mode 100644 index 00000000000..055d33d61e5 --- /dev/null +++ b/lib/deploy/migration_statement_timeout.rb @@ -0,0 +1,13 @@ +module Deploy + module MigrationStatementTimeout + def connection + connection = super + new_statement_timeout = ENV['MIGRATION_STATEMENT_TIMEOUT'] + if new_statement_timeout && !@migration_statement_timeout_set + connection.execute("SET statement_timeout = #{new_statement_timeout.to_i}") + @migration_statement_timeout_set = true + end + connection + end + end +end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 3ab08793df4..1e299d4a06e 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -68,14 +68,6 @@ def self.no_pii_mode? enable_identity_verification? && Figaro.env.profile_proofing_vendor == :mock end - def self.enable_agency_based_uuids? - Figaro.env.enable_agency_based_uuids == 'true' - end - - def self.agencies_with_agency_based_uuids - (Figaro.env.agencies_with_agency_based_uuids || '').split(',').map(&:to_i) - end - def self.enable_saml_cert_rotation? Figaro.env.saml_secret_rotation_enabled == 'true' end diff --git a/lib/proofer_mocks/address_mock.rb b/lib/proofer_mocks/address_mock.rb new file mode 100644 index 00000000000..8faeef62d18 --- /dev/null +++ b/lib/proofer_mocks/address_mock.rb @@ -0,0 +1,12 @@ +class AddressMock < Proofer::Base + attributes :phone + + stage :address + + proof do |applicant, result| + plain_phone = applicant[:phone].gsub(/\D/, '').gsub(/\A1/, '') + if plain_phone == '5555555555' + result.add_error(:phone, 'The phone number could not be verified.') + end + end +end diff --git a/lib/proofer_mocks/resolution_mock.rb b/lib/proofer_mocks/resolution_mock.rb new file mode 100644 index 00000000000..ac907d9ee42 --- /dev/null +++ b/lib/proofer_mocks/resolution_mock.rb @@ -0,0 +1,21 @@ +class ResolutionMock < Proofer::Base + attributes :first_name, :ssn, :zipcode + + stage :resolution + + proof do |applicant, result| + first_name = applicant[:first_name] + + raise 'Failed to contact proofing vendor' if first_name =~ /Fail/i + + if first_name =~ /Bad/i + result.add_error(:first_name, 'Unverified first name.') + + elsif applicant[:ssn] =~ /6666/ + result.add_error(:ssn, 'Unverified SSN.') + + elsif applicant[:zipcode] == '00000' + result.add_error(:zipcode, 'Unverified ZIP code.') + end + end +end diff --git a/lib/proofer_mocks/state_id_mock.rb b/lib/proofer_mocks/state_id_mock.rb new file mode 100644 index 00000000000..0ea04dc31a5 --- /dev/null +++ b/lib/proofer_mocks/state_id_mock.rb @@ -0,0 +1,40 @@ +class StateIdMock < Proofer::Base + SUPPORTED_STATES = %w[ + AR AZ CO DC DE FL IA ID IL IN KY MA MD ME MI MS MT ND NE NJ NM PA SD TX VA WA WI WY + ].freeze + + SUPPORTED_STATE_ID_TYPES = %w[ + drivers_license drivers_permit state_id_card + ].freeze + + attributes :state_id_number, :state_id_type, :state_id_jurisdiction + + stage :state_id + + proof do |applicant, result| + if state_not_supported?(applicant[:state_id_jurisdiction]) + result.add_error(:state_id_jurisdiction, 'The jurisdiction could not be verified') + + elsif invalid_state_id_number?(applicant[:state_id_number]) + result.add_error(:state_id_number, 'The state ID number could not be verified') + + elsif invalid_state_id_type?(applicant[:state_id_type]) + result.add_error(:state_id_type, 'The state ID type could not be verified') + end + end + + private + + def state_not_supported?(state_id_jurisdiction) + !SUPPORTED_STATES.include? state_id_jurisdiction + end + + def invalid_state_id_number?(state_id_number) + state_id_number =~ /\A0*\z/ + end + + def invalid_state_id_type?(state_id_type) + !SUPPORTED_STATE_ID_TYPES.include?(state_id_type) || + state_id_type.nil? + end +end diff --git a/lib/queue_config.rb b/lib/queue_config.rb new file mode 100644 index 00000000000..2853281f34a --- /dev/null +++ b/lib/queue_config.rb @@ -0,0 +1,40 @@ +require_relative './random_tools.rb' + +module Upaya + module QueueConfig + # rubocop:disable Metrics/MethodLength + + # Known acceptable values for config.active_job.queue_adapter + KNOWN_QUEUE_ADAPTERS = %i[sidekiq inline async].freeze + + # Select a queue adapter for use, including possible random weights as + # defined by Figaro.env.queue_adapter_weights (a JSON mapping from queue + # adapters to integer weights).. + def self.choose_queue_adapter + adapter_config = Figaro.env.queue_adapter_weights + + # default to Sidekiq if no config present + return :sidekiq unless adapter_config + + options = JSON.parse(adapter_config, symbolize_names: true) + + options.each_key do |adapter| + unless KNOWN_QUEUE_ADAPTERS.include?(adapter) + raise ArgumentError, "Unknown queue adapter: #{adapter.inspect}" + end + end + + result = Upaya::RandomTools.random_weighted_sample(options) + + logger.info("Selected config.active_job.queue_adapter = #{result.inspect}") + + result + end + + def self.logger + @log ||= Rails.logger || Logger.new(STDOUT) + end + + # rubocop:enable Metrics/MethodLength + end +end diff --git a/lib/random_tools.rb b/lib/random_tools.rb new file mode 100644 index 00000000000..2a9c69eb5bb --- /dev/null +++ b/lib/random_tools.rb @@ -0,0 +1,44 @@ +module Upaya + module RandomTools + # rubocop:disable MethodLength, Style/IfUnlessModifier, Metrics/AbcSize + + # Randomly choose an item among choices with integer weights. For example, + # if passed `{'a' => 3, 'b' => 1}`, the expected return value will approach + # 'a' 75% of the time and 'b' 25% of the time. + # + # @param [Hash{Object => Integer}] choices A mapping from each choice to an + # integer weight. The weights will be relative to the sum of all weights. + # + # @return [Object] One of the keys chosen from the choices hash. + # + def self.random_weighted_sample(choices) + if choices.empty? + raise ArgumentError, 'Cannot choose among empty choices hash' + end + + sum = 0 + choices.each_pair do |item, weight| + unless weight.is_a?(Integer) && weight >= 0 + raise ArgumentError, "Choices must have >= 0 integer weights, " \ + "got #{item.inspect} => #{weight.inspect}" + end + sum += weight + end + + if sum.zero? + raise ArgumentError, 'Must have non-zero weight among choices' + end + + target = rand(sum) + + choices.each_pair do |item, weight| + return item if target < weight + target -= weight + end + + raise NotImplementedError, 'This line should not be reached' + end + + # rubocop:enable MethodLength, Style/IfUnlessModifier, Metrics/AbcSize + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 490bb5793ce..f6284e64ebf 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -255,4 +255,12 @@ def index end end end + + describe '#sign_out' do + it 'deletes the ahoy_visit cookie when signing out' do + expect(request.cookie_jar).to receive(:delete).with('ahoy_visit') + + subject.sign_out + end + end end diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 75339bb93f0..59bb08f235c 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -44,7 +44,8 @@ with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, success: true, client_id: client_id, - errors: {}) + errors: {}, + user_fully_authenticated: true) action end @@ -128,7 +129,8 @@ with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, success: false, client_id: client_id, - errors: hash_including(:prompt)) + errors: hash_including(:prompt), + user_fully_authenticated: true) action end @@ -148,7 +150,8 @@ with(Analytics::OPENID_CONNECT_REQUEST_AUTHORIZATION, success: false, client_id: nil, - errors: hash_including(:client_id)) + errors: hash_including(:client_id), + user_fully_authenticated: true) action end diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 267542dc792..fd6b2f6da55 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -19,7 +19,7 @@ it 'tracks the event when idp-initiated' do stub_analytics - result = { sp_initiated: false, oidc: false } + result = { sp_initiated: false, oidc: false, saml_request_valid: false } expect(@analytics).to receive(:track_event).with(Analytics::LOGOUT_INITIATED, result) @@ -29,7 +29,16 @@ it 'tracks the event when sp-initiated' do allow(controller).to receive(:saml_request).and_return(FakeSamlRequest.new) stub_analytics - result = { sp_initiated: true, oidc: false } + result = { sp_initiated: true, oidc: false, saml_request_valid: true } + + expect(@analytics).to receive(:track_event).with(Analytics::LOGOUT_INITIATED, result) + + delete :logout, params: { SAMLRequest: 'foo' } + end + + it 'tracks the event when sp-initiated and the saml request is not valid' do + stub_analytics + result = { sp_initiated: true, oidc: false, saml_request_valid: false } expect(@analytics).to receive(:track_event).with(Analytics::LOGOUT_INITIATED, result) @@ -38,6 +47,16 @@ end describe 'POST /api/saml/logout' do + context 'when there is an invalid SAML packet' do + let(:user) { create(:user, :signed_up) } + + it 'responds with "400 Bad Request"' do + sign_in user + + post :logout, params: { SAMLRequest: 'foo' } + expect(response.status).to eq(400) + end + end context 'when SAML response is not successful' do let(:user) { create(:user, :signed_up) } diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 3ab52e3d4e0..0080eab374f 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -39,7 +39,7 @@ stub_analytics analytics_hash = { context: 'authentication', - method: 'sms', + multi_factor_auth_method: 'sms', confirmation_for_phone_change: false, } @@ -79,7 +79,7 @@ errors: {}, confirmation_for_phone_change: false, context: 'authentication', - method: 'sms', + multi_factor_auth_method: 'sms', } stub_analytics @@ -125,7 +125,7 @@ errors: {}, confirmation_for_phone_change: false, context: 'authentication', - method: 'sms', + multi_factor_auth_method: 'sms', } stub_analytics @@ -169,7 +169,7 @@ errors: {}, confirmation_for_phone_change: false, context: 'authentication', - method: 'sms', + multi_factor_auth_method: 'sms', } stub_analytics @@ -298,7 +298,7 @@ errors: {}, confirmation_for_phone_change: true, context: 'confirmation', - method: 'sms', + multi_factor_auth_method: 'sms', } expect(@analytics).to have_received(:track_event). @@ -340,7 +340,7 @@ errors: {}, confirmation_for_phone_change: true, context: 'confirmation', - method: 'sms', + multi_factor_auth_method: 'sms', } expect(@analytics).to have_received(:track_event). @@ -376,7 +376,7 @@ success: true, errors: {}, context: 'confirmation', - method: 'sms', + multi_factor_auth_method: 'sms', confirmation_for_phone_change: false, } @@ -467,7 +467,7 @@ errors: {}, confirmation_for_phone_change: false, context: 'idv', - method: 'sms', + multi_factor_auth_method: 'sms', } expect(@analytics).to have_received(:track_event). @@ -533,7 +533,7 @@ errors: {}, confirmation_for_phone_change: false, context: 'idv', - method: 'sms', + multi_factor_auth_method: 'sms', } expect(@analytics).to have_received(:track_event). diff --git a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb index c91467a1e3b..3615ee49fb1 100644 --- a/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/personal_key_verification_controller_spec.rb @@ -18,7 +18,7 @@ it 'tracks the page visit' do stub_sign_in_before_2fa stub_analytics - analytics_hash = { context: 'authentication', method: 'personal key' } + analytics_hash = { context: 'authentication' } expect(@analytics).to receive(:track_event). with(Analytics::MULTI_FACTOR_AUTH_ENTER_PERSONAL_KEY_VISIT, analytics_hash) diff --git a/spec/controllers/verify/confirmations_controller_spec.rb b/spec/controllers/verify/confirmations_controller_spec.rb index ea2b7368f57..cc1a4c6f183 100644 --- a/spec/controllers/verify/confirmations_controller_spec.rb +++ b/spec/controllers/verify/confirmations_controller_spec.rb @@ -13,7 +13,7 @@ def stub_idv_session ) idv_session.applicant = idv_session.vendor_params idv_session.normalized_applicant_params = { first_name: 'Somebody' } - idv_session.resolution_successful = resolution.success? + idv_session.resolution_successful = true user.unlock_user_access_key(password) profile_maker = Idv::ProfileMaker.new( applicant: applicant, @@ -32,7 +32,7 @@ def stub_idv_session let(:password) { 'sekrit phrase' } let(:user) { create(:user, :signed_up, password: password) } let(:applicant) do - Proofer::Applicant.new( + { first_name: 'Some', last_name: 'One', address1: '123 Any St', @@ -40,11 +40,9 @@ def stub_idv_session city: 'Anywhere', state: 'KS', zipcode: '66666' - ) + } end - let(:normalized_applicant) { Proofer::Applicant.new first_name: 'Somebody' } - let(:agent) { Proofer::Agent.new vendor: :mock } - let(:resolution) { agent.start applicant } + let(:normalized_applicant) { { first_name: 'Somebody' } } let(:profile) { subject.idv_session.profile } describe 'before_actions' do diff --git a/spec/controllers/verify/phone_controller_spec.rb b/spec/controllers/verify/phone_controller_spec.rb index e312a0505a6..5cb5eb84e5b 100644 --- a/spec/controllers/verify/phone_controller_spec.rb +++ b/spec/controllers/verify/phone_controller_spec.rb @@ -77,7 +77,7 @@ end it 'tracks form error and does not make a vendor API call' do - expect(Idv::SubmitIdvJob).to_not receive(:submit_phone_job) + expect(Idv::Job).to_not receive(:submit) put :create, params: { idv_phone_form: { phone: '703' } } @@ -270,10 +270,10 @@ user = build(:user, phone: good_phone, phone_confirmed_at: Time.zone.now) stub_verify_steps_one_and_two(user) - expect(Idv::SubmitIdvJob).to receive(:new).with( - idv_session: subject.idv_session, - vendor_params: normalized_phone - ).and_call_original + subject.params = { phone: normalized_phone } + expect(Idv::Job).to receive(:submit). + with(subject.idv_session, [:address]). + and_call_original put :create, params: { idv_phone_form: { phone: good_phone } } end diff --git a/spec/controllers/verify/sessions_controller_spec.rb b/spec/controllers/verify/sessions_controller_spec.rb index 8dd0024b187..7f50ba999d2 100644 --- a/spec/controllers/verify/sessions_controller_spec.rb +++ b/spec/controllers/verify/sessions_controller_spec.rb @@ -30,7 +30,7 @@ let(:idv_session) do Idv::Session.new(user_session: subject.user_session, current_user: user, issuer: nil) end - let(:normalized_applicant) { Proofer::Applicant.new(user_attrs) } + let(:normalized_applicant) { user_attrs } describe 'before_actions' do it 'includes before_actions from AccountStateChecker' do @@ -194,7 +194,7 @@ success: false, errors: { timed_out: ['Timed out waiting for vendor response'] }, idv_attempts_exceeded: false, - vendor: { reasons: [] }, + vendor: { messages: [] }, } expect(@analytics).to have_received(:track_event).with( @@ -221,7 +221,7 @@ Idv::VendorResult.new( success: false, errors: { first_name: ['Unverified first name.'] }, - reasons: ['The name was suspicious'] + messages: ['The name was suspicious'] ) end @@ -242,7 +242,7 @@ errors: { first_name: ['Unverified first name.'], }, - vendor: { reasons: ['The name was suspicious'] }, + vendor: { messages: ['The name was suspicious'] }, } expect(@analytics).to have_received(:track_event). @@ -296,7 +296,7 @@ Idv::VendorResult.new( success: false, errors: { agent: [exception_msg] }, - reasons: [exception_msg] + messages: [exception_msg] ) end @@ -309,7 +309,7 @@ errors: { agent: [exception_msg], }, - vendor: { reasons: [exception_msg] }, + vendor: { messages: [exception_msg] }, } expect(@analytics).to have_received(:track_event). @@ -322,7 +322,7 @@ let(:result) do Idv::VendorResult.new( success: true, - reasons: ['Everything looks good'], + messages: ['Everything looks good'], normalized_applicant: normalized_applicant ) end @@ -334,7 +334,7 @@ success: true, idv_attempts_exceeded: false, errors: {}, - vendor: { reasons: ['Everything looks good'] }, + vendor: { messages: ['Everything looks good'] }, } expect(@analytics).to have_received(:track_event). diff --git a/spec/features/idv/account_creation_spec.rb b/spec/features/idv/account_creation_spec.rb deleted file mode 100644 index 1c9b5b72868..00000000000 --- a/spec/features/idv/account_creation_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails_helper' - -feature 'account creation after LOA3 request', idv_job: true do - include SamlAuthHelper - include IdvHelper - - context 'choosing USPS address verification' do - it_behaves_like 'selecting usps address verification method', :saml - it_behaves_like 'selecting usps address verification method', :oidc - end - - context 'choosing phone address verification otp delivery method' do - it_behaves_like 'idv otp delivery method selection', :saml - it_behaves_like 'idv otp delivery method selection', :oidc - end - - context 'entering state id data' do - it_behaves_like 'idv state id data entry', :saml - it_behaves_like 'idv state id data entry', :oidc - end -end diff --git a/spec/features/idv/flow_spec.rb b/spec/features/idv/flow_spec.rb deleted file mode 100644 index 5838704695f..00000000000 --- a/spec/features/idv/flow_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -require 'rails_helper' - -feature 'IdV session', idv_job: true do - include IdvHelper - - context 'landing page' do - before do - sign_in_and_2fa_user - visit verify_path - end - - scenario 'proceed to verify identity' do - click_link 'Yes' - - expect(page).to have_content(t('idv.titles.sessions')) - end - end - - context 'verification session' do - scenario 'normal flow' do - user = sign_in_and_2fa_user - - visit verify_session_path - - fill_out_idv_form_ok - click_idv_continue - - expect(page).to have_content( - t('idv.messages.sessions.success', - pii_message: t('idv.messages.sessions.pii')) - ) - - click_idv_address_choose_phone - fill_out_phone_form_ok(user.phone) - click_idv_continue - fill_in :user_password, with: user_password - click_continue - - expect(current_url).to eq verify_confirmations_url - expect(page).to have_content(t('headings.personal_key')) - click_acknowledge_personal_key - - expect(current_url).to eq(account_url) - expect(page).to have_content('José One') - expect(page).to have_content('123 Main St') - expect(user.reload.active_profile).to be_a(Profile) - end - - scenario 'profile steps is not re-entrant and are sticky on failure', :js do - user = sign_in_and_2fa_user - - visit verify_session_path - - first_ssn_value = '666-66-6666' - second_ssn_value = '666-66-1234' - good_phone_value = '415-555-9999' - good_phone_formatted = '+1 (415) 555-9999' - bad_phone_formatted = '+1 (555) 555-5555' - - # we start with blank form - expect(page).to_not have_selector("input[value='#{first_ssn_value}']") - - fill_out_idv_form_fail - click_idv_continue - - # failure reloads the form and shows warning modal - expect(current_path).to eq verify_session_result_path - expect(find('.modal-warning').text).to match t('idv.modal.sessions.heading') - click_button t('idv.modal.button.warning') - - fill_out_idv_form_ok - click_idv_continue - - # address mechanism choice - click_idv_address_choose_phone - - # success advances to next step - expect(current_path).to eq verify_phone_path - - # we start with blank form - expect(page).to_not have_selector("input[value='#{bad_phone_formatted}']") - - fill_out_phone_form_fail - click_idv_continue - - # failure reloads the same sticky form - expect(current_path).to eq verify_phone_result_path - expect(page).to have_css('.modal-warning', text: t('idv.modal.phone.heading')) - click_button t('idv.modal.button.warning') - expect(page).to have_selector("input[value='#{bad_phone_formatted}']") - - fill_out_phone_form_ok(good_phone_value) - click_idv_continue - choose_idv_otp_delivery_method_sms - enter_correct_otp_code_for_user(user) - - page.find('.accordion').click - - # success advances to next step - expect(page).to have_content(t('idv.titles.session.review')) - expect(page).to have_content(second_ssn_value) - expect(page).to_not have_content(first_ssn_value) - expect(page).to have_content(good_phone_formatted) - expect(page).to_not have_content(bad_phone_formatted) - end - - scenario 'phone step is re-entrant', :js do - phone = '(555) 555-5000' - different_phone = '(777) 777-7000' - user = sign_in_and_2fa_user - - visit verify_session_path - fill_out_idv_form_ok - click_idv_continue - click_idv_address_choose_phone - fill_out_phone_form_ok(phone) - click_idv_continue - choose_idv_otp_delivery_method_sms - - click_link t('forms.two_factor.try_again') - - expect(page.find('#idv_phone_form_phone').value).to eq("+1 #{phone}") - expect(current_path).to eq(verify_phone_path) - - fill_out_phone_form_ok(different_phone) - click_idv_continue - choose_idv_otp_delivery_method_sms - - # Verify that OTP confirmation can't be skipped - visit verify_review_path - expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :sms) - - enter_correct_otp_code_for_user(user) - - page.find('.accordion').click - - expect(page).to_not have_content(phone) - expect(page).to have_content(different_phone) - end - - scenario 'closing previous address accordion clears inputs and toggles header', js: true do - _user = sign_in_and_2fa_user - - visit verify_session_path - expect(page).to have_css('.accordion-header-controls', - text: t('idv.form.previous_address_add')) - - click_accordion - expect(page).to have_css('.accordion-header', text: t('links.remove')) - - fill_out_idv_previous_address_ok - expect(find('#profile_prev_address1').value).to eq '456 Other Ave' - - click_accordion - click_accordion - - expect(find('#profile_prev_address1').value).to eq '' - end - - scenario 'attempting to skip OTP phone confirmation redirects to OTP confirmation', :js do - different_phone = '555-555-9876' - user = sign_in_live_with_2fa - visit verify_session_path - - fill_out_idv_form_ok - click_idv_continue - click_idv_address_choose_phone - fill_out_phone_form_ok(different_phone) - click_idv_continue - - # Modify URL to skip phone confirmation - visit verify_review_path - user.reload - - expect(current_path).to eq(login_two_factor_path(otp_delivery_preference: :sms)) - expect(user.profiles).to be_empty - end - end - - def click_accordion - find('.accordion-header-controls[aria-controls="previous-address"]').click - end -end diff --git a/spec/features/idv/phone_spec.rb b/spec/features/idv/phone_spec.rb deleted file mode 100644 index 29a86a6be65..00000000000 --- a/spec/features/idv/phone_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'rails_helper' - -feature 'Verify phone' do - include IdvHelper - - context 'Idv phone and user phone are different', idv_job: true do - scenario 'prompts to confirm phone' do - user = create( - :user, :signed_up, - phone: '+1 (416) 555-0190', - password: Features::SessionHelper::VALID_PASSWORD - ) - - sign_in_and_2fa_user(user) - visit verify_session_path - complete_idv_profile_with_phone('555-555-0000') - - fill_in 'code', with: 'not a valid code 😟' - click_submit_default - expect(page).to have_link t('forms.two_factor.try_again'), href: verify_phone_path - - enter_correct_otp_code_for_user(user) - fill_in :user_password, with: user_password - click_continue - click_acknowledge_personal_key - - expect(current_path).to eq account_path - end - - scenario 'user cannot re-enter phone step and change phone after confirmation', :idv_job do - user = sign_in_and_2fa_user - - visit verify_session_path - fill_out_idv_form_ok - click_idv_continue - click_idv_address_choose_phone - fill_out_phone_form_ok - click_idv_continue - choose_idv_otp_delivery_method_sms - enter_correct_otp_code_for_user(user) - - visit verify_phone_path - expect(current_path).to eq(verify_review_path) - end - end - - def complete_idv_profile_with_phone(phone) - fill_out_idv_form_ok - click_idv_continue - click_idv_address_choose_phone - fill_out_phone_form_ok(phone) - click_idv_continue - choose_idv_otp_delivery_method_sms - end -end diff --git a/spec/features/idv/previous_address_spec.rb b/spec/features/idv/previous_address_spec.rb index 3fae19126b1..eb3f75f6ceb 100644 --- a/spec/features/idv/previous_address_spec.rb +++ b/spec/features/idv/previous_address_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' feature 'IdV with previous address filled in', idv_job: true do - include IdvHelper + include IdvStepHelper let(:bad_zipcode) { '00000' } let(:current_address) { '123 Main St' } @@ -12,14 +12,6 @@ def expect_to_stay_on_verify_session_page expect(page).to have_selector("input[value='#{bad_zipcode}']") end - def expect_bad_previous_address_to_fail - fill_out_idv_form_ok - fill_out_idv_previous_address_fail - click_idv_continue - - expect_to_stay_on_verify_session_page - end - def expect_bad_current_address_to_fail fill_out_idv_previous_address_ok fill_out_idv_form_fail @@ -42,12 +34,35 @@ def expect_current_address_in_profile(user) expect(page).to_not have_content(previous_address) end - it 'fails when either address has bad value, prefers current address in profile' do - user = sign_in_and_2fa_user - visit verify_session_path + it 'fails when current address has bad value, prefers current address in profile' do + user = user_with_2fa + start_idv_from_sp + complete_idv_steps_before_profile_step(user) - expect_bad_previous_address_to_fail expect_bad_current_address_to_fail expect_current_address_in_profile(user) end + + it 'closing previous address accordion clears inputs and toggles header', :js do + start_idv_from_sp + complete_idv_steps_before_profile_step + + expect(page).to have_css('.accordion-header-controls', + text: t('idv.form.previous_address_add')) + + click_accordion + expect(page).to have_css('.accordion-header', text: t('links.remove')) + + fill_out_idv_previous_address_ok + expect(find('#profile_prev_address1').value).to eq '456 Other Ave' + + click_accordion + click_accordion + + expect(find('#profile_prev_address1').value).to eq '' + end + + def click_accordion + find('.accordion-header-controls[aria-controls="previous-address"]').click + end end diff --git a/spec/support/idv_examples/state_id_data.rb b/spec/features/idv/state_id_data_spec.rb similarity index 64% rename from spec/support/idv_examples/state_id_data.rb rename to spec/features/idv/state_id_data_spec.rb index c77563a33c0..b501dc0fb32 100644 --- a/spec/support/idv_examples/state_id_data.rb +++ b/spec/features/idv/state_id_data_spec.rb @@ -1,10 +1,15 @@ -shared_examples 'idv state id data entry' do |sp| - it 'renders an error for unverifiable state id number', :email do - visit_idp_from_sp_with_loa3(sp) - register_user +require 'rails_helper' + +feature 'idv state id data entry', :idv_job do + include IdvStepHelper - visit verify_session_path + before do + start_idv_from_sp + complete_idv_steps_before_profile_step fill_out_idv_form_ok + end + + it 'renders an error for unverifiable state id number', :email do fill_in :profile_state_id_number, with: '000000000' click_idv_continue @@ -13,14 +18,9 @@ end it 'renders an error for blank state id number and does not submit a job', :email do - expect(Idv::ProfileJob).to_not receive(:perform_now) - expect(Idv::ProfileJob).to_not receive(:perform_later) - - visit_idp_from_sp_with_loa3(sp) - register_user + expect(Idv::ProoferJob).to_not receive(:perform_now) + expect(Idv::ProoferJob).to_not receive(:perform_later) - visit verify_session_path - fill_out_idv_form_ok fill_in :profile_state_id_number, with: '' click_idv_continue @@ -29,14 +29,9 @@ end it 'renders an error for unsupported jurisdiction and does not submit a job', :email do - expect(Idv::ProfileJob).to_not receive(:perform_now) - expect(Idv::ProfileJob).to_not receive(:perform_later) - - visit_idp_from_sp_with_loa3(sp) - register_user + expect(Idv::ProoferJob).to_not receive(:perform_now) + expect(Idv::ProoferJob).to_not receive(:perform_later) - visit verify_session_path - fill_out_idv_form_ok select 'Alabama', from: 'profile_state' click_idv_continue @@ -45,11 +40,6 @@ end it 'allows selection of different state id types', :email do - visit_idp_from_sp_with_loa3(sp) - register_user - - visit verify_session_path - fill_out_idv_form_ok select t('idv.form.state_id_type.drivers_permit'), from: 'profile_state_id_type' click_idv_continue diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index eec7853c11b..be48d9f2acc 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -13,6 +13,17 @@ expect(page).to have_content(t('idv.titles.otp_delivery_method')) expect(page).to have_current_path(verify_otp_delivery_method_path) end + + it 'redirects to the confirmation step when the phone matches the 2fa phone number' do + user = user_with_2fa + start_idv_from_sp + complete_idv_steps_before_phone_step(user) + fill_out_phone_form_ok(user.phone) + click_idv_continue + + expect(page).to have_content(t('idv.titles.session.review')) + expect(page).to have_current_path(verify_review_path) + end end context 'after submitting valid information' do diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 0d4fce612c3..778fea3ed32 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -8,11 +8,6 @@ oidc_end_client_secret_jwt(prompt: 'select_account') end - it 'succeeds with new agency based uuids' do - allow(FeatureManagement).to receive(:enable_agency_based_uuids?).and_return(true) - oidc_end_client_secret_jwt(prompt: 'select_account') - end - it 'succeeds in returning back to sp with prompt select_account and prior session' do user = oidc_end_client_secret_jwt(prompt: 'select_account') oidc_end_client_secret_jwt(prompt: 'select_account', user: user, redirs_to: '/auth/result') diff --git a/spec/features/saml/loa1_sso_spec.rb b/spec/features/saml/loa1_sso_spec.rb index 087eb5f24f5..dc593198337 100644 --- a/spec/features/saml/loa1_sso_spec.rb +++ b/spec/features/saml/loa1_sso_spec.rb @@ -175,19 +175,6 @@ end end - context 'fully signed up user is signed in with email/pwd and new agency based uuids' do - it 'prompts to enter OTP' do - allow(FeatureManagement).to receive(:enable_agency_based_uuids?).and_return(true) - user = create(:user, :signed_up) - sign_in_user(user) - - saml_authn_request = auth_request.create(saml_settings) - visit saml_authn_request - - expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms') - end - end - context 'user that has not yet set up 2FA is signed in with email and password only' do it 'prompts to set up 2FA' do sign_in_user diff --git a/spec/features/saml/sp_initiated_slo_spec.rb b/spec/features/saml/sp_initiated_slo_spec.rb index c6bf4107a24..187c999de90 100644 --- a/spec/features/saml/sp_initiated_slo_spec.rb +++ b/spec/features/saml/sp_initiated_slo_spec.rb @@ -88,7 +88,6 @@ let(:user) { create(:user, :signed_up) } before do - allow(FeatureManagement).to receive(:enable_agency_based_uuids?).and_return(true) sign_in_and_2fa_user(user) visit sp1_authnrequest diff --git a/spec/features/visitors/i18n_spec.rb b/spec/features/visitors/i18n_spec.rb index f6de7dbb04a..e6c8ab9afc1 100644 --- a/spec/features/visitors/i18n_spec.rb +++ b/spec/features/visitors/i18n_spec.rb @@ -79,4 +79,17 @@ expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') end end + + context 'visit homepage with host parameter' do + it 'does not include the host parameter in the language link URLs' do + visit '/fr?host=test.com' + + %w[en es fr].each do |locale| + expect(page).to_not have_link( + t("i18n.locale.#{locale}"), + href: "http://test.com/#{locale}" + ) + end + end + end end diff --git a/spec/forms/openid_connect_logout_form_spec.rb b/spec/forms/openid_connect_logout_form_spec.rb index 642943d8f96..1a385df42ed 100644 --- a/spec/forms/openid_connect_logout_form_spec.rb +++ b/spec/forms/openid_connect_logout_form_spec.rb @@ -45,11 +45,6 @@ it 'has a successful response' do expect(result).to be_success end - - it 'has a successful response when agency based uuids are enabled' do - allow(FeatureManagement).to receive(:enable_agency_based_uuids?).and_return(true) - expect(result).to be_success - end end context 'with an invalid form' do @@ -118,13 +113,6 @@ expect(form.errors[:id_token_hint]). to include(t('openid_connect.logout.errors.id_token_hint')) end - - it 'is not valid when agency based uuids are enabled' do - allow(FeatureManagement).to receive(:enable_agency_based_uuids?).and_return(true) - expect(valid?).to eq(false) - expect(form.errors[:id_token_hint]). - to include(t('openid_connect.logout.errors.id_token_hint')) - end end context 'with an expired, but otherwise valid id_token_hint' do diff --git a/spec/jobs/idv/phone_job_spec.rb b/spec/jobs/idv/phone_job_spec.rb deleted file mode 100644 index 72cfd474887..00000000000 --- a/spec/jobs/idv/phone_job_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'rails_helper' - -describe Idv::PhoneJob do - include ProoferJobHelper - - describe '#perform' do - let(:result_id) { SecureRandom.uuid } - let(:applicant_json) { { first_name: 'Jean-Luc', last_name: 'Picard' }.to_json } - let(:vendor_params) { '5555550000' } - - context 'when verification succeeds' do - it 'should save a successful result' do - Idv::PhoneJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json, - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(true) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(false) - expect(result.reasons).to eq(['Good number']) - expect(result.errors).to eq({}) - end - end - - context 'when verification fails' do - let(:vendor_params) { '5555555555' } - - it 'should save an unsuccessful result' do - Idv::PhoneJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json, - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(false) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(false) - expect(result.reasons).to eq(['Bad number']) - expect(result.errors).to eq(phone: 'The phone number could not be verified.') - end - end - - context 'when the idv agent raises' do - before do - agent = instance_double(Idv::Agent) - allow(agent).to receive(:submit_phone).and_raise(RuntimeError, '🔥🔥🔥') - allow(Idv::Agent).to receive(:new).and_return(agent) - end - - it 'should rescue from errors and save a failed job result' do - expect do - Idv::PhoneJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json, - ) - end.to raise_error(RuntimeError, '🔥🔥🔥') - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(false) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(true) - end - end - - it 'selects the proofer vendor based on the config' do - mock_proofer_job_agent(config: :phone_proofing_vendor, vendor: 'fancy_vendor') - - Idv::PhoneJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json, - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(Idv::Agent).to have_received(:new).with(hash_including(vendor: :fancy_vendor)) - expect(result).to be_a(Idv::VendorResult) - end - end -end diff --git a/spec/jobs/idv/profile_job_spec.rb b/spec/jobs/idv/profile_job_spec.rb deleted file mode 100644 index 082f4b35629..00000000000 --- a/spec/jobs/idv/profile_job_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -require 'rails_helper' - -describe Idv::ProfileJob do - include ProoferJobHelper - - describe '#perform' do - let(:result_id) { SecureRandom.uuid } - let(:applicant_json) { { first_name: 'Jean-Luc', last_name: 'Picard' }.to_json } - let(:vendor_params) do - { - dob: '07/13/2035', - state: 'VA', - state_id_number: '123456789', - state_id_type: 'drivers_license', - } - end - - it 'uses the state vendor params as the state id jurisdiction' do - agent = Idv::Agent.new(vendor: :mock, applicant: {}) - allow(Idv::Agent).to receive(:new).and_return(agent).twice - - expect(agent).to receive(:submit_state_id). - with(hash_including(state_id_jurisdiction: vendor_params[:state])). - and_call_original - - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result).to be_a(Idv::VendorResult) - expect(result.success?).to eq(true) - end - - context 'when resolution and state id confirmation succeed' do - it 'should save a successful result' do - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(true) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(false) - expect(result.normalized_applicant.first_name).to eq('JEAN-LUC') - expect(result.normalized_applicant.last_name).to eq('PICARD') - expect(result.reasons).to eq(['Everything looks good', 'valid state ID']) - expect(result.errors).to eq({}) - end - end - - context 'when resolution fails' do - it 'should save an unsuccessful result and not call state id proofer' do - applicant = Proofer::Applicant.new(first_name: 'Bad') - agent = Idv::Agent.new(vendor: :mock, applicant: applicant) - allow(agent).to receive(:start).and_call_original - allow(agent).to receive(:submit_state_id).and_call_original - allow(Idv::Agent).to receive(:new).and_return(agent).twice - - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(false) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(false) - expect(result.reasons).to eq(['The name was suspicious']) - expect(result.errors).to eq(first_name: 'Unverified first name.') - expect(agent).to have_received(:start) - expect(agent).to_not have_received(:submit_state_id) - end - end - - context 'when state id confirmation fails' do - let(:vendor_params) { super().merge(state_id_number: '000000000') } - - it 'saves an unsuccessful result' do - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(false) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(false) - expect(result.reasons).to eq(['Everything looks good', 'invalid state id number']) - expect(result.errors).to eq(state_id_number: 'The state ID number could not be verified') - end - end - - context 'when the idv agent raises' do - before do - agent = instance_double(Idv::Agent) - allow(agent).to receive(:start).and_raise(RuntimeError, '🔥🔥🔥') - allow(Idv::Agent).to receive(:new).and_return(agent) - end - - it 'should rescue from errors and save a failed job result' do - expect do - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - end.to raise_error(RuntimeError, '🔥🔥🔥') - result = VendorValidatorResultStorage.new.load(result_id) - - expect(result.success?).to eq(false) - expect(result.timed_out?).to eq(false) - expect(result.job_failed?).to eq(true) - end - end - - it 'selects the vendors based on the config' do - mock_proofer_job_agent(config: :profile_proofing_vendor, vendor: 'fancy_vendor') - mock_proofer_job_agent(config: :state_id_proofing_vendor, vendor: 'fancier_vendor') - - Idv::ProfileJob.perform_now( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant_json - ) - result = VendorValidatorResultStorage.new.load(result_id) - - expect(Idv::Agent).to have_received(:new).with(hash_including(vendor: :fancy_vendor)) - expect(Idv::Agent).to have_received(:new).with(hash_including(vendor: :fancier_vendor)) - expect(result).to be_a(Idv::VendorResult) - end - end -end diff --git a/spec/jobs/idv/proofer_job_spec.rb b/spec/jobs/idv/proofer_job_spec.rb new file mode 100644 index 00000000000..58287c838af --- /dev/null +++ b/spec/jobs/idv/proofer_job_spec.rb @@ -0,0 +1,115 @@ +require 'rails_helper' + +describe Idv::ProoferJob do + describe '#perform' do + context 'without mocking the agent' do + let(:result_id) { SecureRandom.uuid } + let(:applicant) { { first_name: 'Jean-Luc', ssn: '123456789', zipcode: '11111' } } + let(:stages) { %i[resolution] } + + it 'works' do + Idv::ProoferJob.perform_now( + result_id: result_id, + applicant_json: applicant.to_json, + stages: stages.to_json + ) + + result = VendorValidatorResultStorage.new.load(result_id) + + expect(result.success?).to eq(true) + expect(result.timed_out?).to eq(false) + expect(result.job_failed?).to eq(false) + expect(result.messages).to be_empty + expect(result.errors).to be_empty + end + end + + context 'when mocking the agent' do + let(:result_id) { SecureRandom.uuid } + let(:applicant) { { first_name: 'Jean-Luc', last_name: 'Picard' } } + let(:stages) { %i[phone] } + let(:agent) { instance_double(Idv::Agent) } + let(:proofer_results) { {} } + + before do + allow(agent).to receive(:proof).and_return(proofer_results) + allow(Idv::Agent).to receive(:new).and_return(agent) + end + + subject do + Idv::ProoferJob.perform_now( + result_id: result_id, + applicant_json: applicant.to_json, + stages: stages.to_json + ) + end + + shared_examples 'a proofer job' do + it 'uses the Idv::Agent' do + subject + + expect(Idv::Agent).to have_received(:new).with(applicant) + expect(agent).to have_received(:proof).with(*stages) + end + end + + context 'when verification succeeds' do + let(:proofer_results) { { success: true, messages: ['a reason'] } } + + it_behaves_like 'a proofer job' + + it 'should save a successful result' do + subject + + result = VendorValidatorResultStorage.new.load(result_id) + + expect(result.success?).to eq(true) + expect(result.timed_out?).to eq(false) + expect(result.job_failed?).to eq(false) + expect(result.messages).to eq(['a reason']) + expect(result.errors).to be_empty + end + end + + context 'when verification fails' do + let(:proofer_results) do + { + success: false, + messages: ['Bad number'], + errors: { phone: 'The phone number could not be verified.' }, + } + end + + it_behaves_like 'a proofer job' + + it 'should save an unsuccessful result' do + subject + + result = VendorValidatorResultStorage.new.load(result_id) + + expect(result.success?).to eq(false) + expect(result.timed_out?).to eq(false) + expect(result.job_failed?).to eq(false) + expect(result.messages).to eq(['Bad number']) + expect(result.errors).to eq(phone: 'The phone number could not be verified.') + end + end + + context 'when the idv agent raises' do + before do + allow(agent).to receive(:proof).and_raise(RuntimeError, '🔥🔥🔥') + end + + it 'should rescue from errors and save a failed job result' do + expect { subject }.to raise_error(RuntimeError, '🔥🔥🔥') + + result = VendorValidatorResultStorage.new.load(result_id) + + expect(result.success?).to eq(false) + expect(result.timed_out?).to eq(false) + expect(result.job_failed?).to eq(true) + end + end + end + end +end diff --git a/spec/lib/config_validator_spec.rb b/spec/lib/config_validator_spec.rb index 86101d6afeb..554a48cb50e 100644 --- a/spec/lib/config_validator_spec.rb +++ b/spec/lib/config_validator_spec.rb @@ -2,13 +2,7 @@ describe ConfigValidator do describe '#validate' do - let(:env) do - { - 'phone_proofing_vendor' => 'mock', - 'profile_proofing_vendor' => 'mock', - 'state_id_proofing_vendor' => 'mock', - } - end + let(:env) { {} } it 'raises if one or more candidate key values is set to yes or no' do env.merge!( @@ -30,16 +24,6 @@ ) end - it 'raises if a non empty key is empty' do - env.delete('phone_proofing_vendor') - mimic_figaro - - expect { ConfigValidator.new.validate(env) }.to raise_error( - RuntimeError, - 'These configs are required and were empty: phone_proofing_vendor' - ) - end - def mimic_figaro # Figaro sets 2 environment variables for each configuration: # 1 with and 1 without the Figaro prefix. Settings that don't diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index d23db1d1b81..68e2b83d086 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -226,58 +226,4 @@ end end end - - describe '#enable_agency_based_uuids?' do - context 'when enabled' do - before do - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('true') - end - - it 'enables the feature' do - expect(FeatureManagement.enable_agency_based_uuids?).to eq(true) - end - end - - context 'when disabled' do - before do - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('false') - end - - it 'disables the feature' do - expect(FeatureManagement.enable_agency_based_uuids?).to eq(false) - end - end - end - - describe 'agencies_with_agency_based_uuids' do - context 'when multiple agencies are enabled' do - before do - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1,2,3') - end - - it 'it returns an array of agencies' do - expect(FeatureManagement.agencies_with_agency_based_uuids).to eq([1, 2, 3]) - end - end - - context 'when one agency is enabled' do - before do - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1') - end - - it 'returns an array containing a single agency' do - expect(FeatureManagement.agencies_with_agency_based_uuids).to eq([1]) - end - end - - context 'when blank' do - before do - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('') - end - - it 'returns an empty array' do - expect(FeatureManagement.agencies_with_agency_based_uuids).to eq([]) - end - end - end end diff --git a/spec/lib/queue_config_spec.rb b/spec/lib/queue_config_spec.rb new file mode 100644 index 00000000000..78cb1a3b4a1 --- /dev/null +++ b/spec/lib/queue_config_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Upaya::QueueConfig do + describe '.choose_queue_adapter' do + it 'raises ArgumentError given invalid choice' do + expect(Figaro.env).to receive(:queue_adapter_weights).and_return('{"invalid": 1}') + expect { + Upaya::QueueConfig.choose_queue_adapter + }.to raise_error(ArgumentError, /Unknown queue adapter/) + end + + it 'handles sidekiq' do + expect(Figaro.env).to receive(:queue_adapter_weights).and_return('{"sidekiq": 1}') + expect(Upaya::QueueConfig.choose_queue_adapter).to eq :sidekiq + end + it 'handles async' do + expect(Figaro.env).to receive(:queue_adapter_weights).and_return('{"async": 1, "sidekiq": 0}') + expect(Upaya::QueueConfig.choose_queue_adapter).to eq :async + end + it 'handles inline' do + expect(Figaro.env).to receive(:queue_adapter_weights).and_return('{"inline": 1}') + expect(Upaya::QueueConfig.choose_queue_adapter).to eq :inline + end + end +end diff --git a/spec/lib/random_tools_spec.rb b/spec/lib/random_tools_spec.rb new file mode 100644 index 00000000000..1bd9deb5361 --- /dev/null +++ b/spec/lib/random_tools_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe Upaya::RandomTools do + describe '#random_weighted_sample' do + it 'raises ArgumentError given empty choices' do + expect { + Upaya::RandomTools.random_weighted_sample({}) + }.to raise_error(ArgumentError, /empty choices/) + end + + it 'handles equal weights -- 0' do + expect(Upaya::RandomTools).to receive(:rand).with(2).and_return(0) + input = { A: 1, B: 1 } + expect(Upaya::RandomTools.random_weighted_sample(input)).to eq :A + end + it 'handles equal weights -- 1' do + expect(Upaya::RandomTools).to receive(:rand).with(2).and_return(1) + input = { A: 1, B: 1 } + expect(Upaya::RandomTools.random_weighted_sample(input)).to eq :B + end + + it 'handles complex weights' do + input = { A: 1, B: 1, C: 4, D: 2, E: 2 } + [ + [0, :A], + [1, :B], + [2, :C], + [3, :C], + [4, :C], + [5, :C], + [6, :D], + [7, :D], + [8, :E], + [9, :E], + ].each do |rand_result, expected_return_value| + expect(Upaya::RandomTools).to receive(:rand).with(10).and_return(rand_result) + expect(Upaya::RandomTools.random_weighted_sample(input)).to eq expected_return_value + end + end + + it 'rejects non-integer weights' do + expect { + Upaya::RandomTools.random_weighted_sample(a: 1.5) + }.to raise_error(ArgumentError, /integer/) + end + + it 'rejects negative weights' do + expect { + Upaya::RandomTools.random_weighted_sample(a: 10, b: -1) + }.to raise_error(ArgumentError, />= 0/) + end + + it 'rejects weights sum to zero' do + expect { + Upaya::RandomTools.random_weighted_sample(a: 0) + }.to raise_error(ArgumentError, /non-zero/) + end + end +end diff --git a/spec/models/agency_identity_spec.rb b/spec/models/agency_identity_spec.rb index f3383a614a2..36f0974cf49 100644 --- a/spec/models/agency_identity_spec.rb +++ b/spec/models/agency_identity_spec.rb @@ -9,18 +9,4 @@ it { is_expected.to validate_presence_of(:uuid) } end - - describe '#agency_enabled?' do - it 'returns true if the agency is enabled' do - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1') - ai = AgencyIdentity.new(agency_id: 1, user_id: 1, uuid: 'UUID1') - expect(ai.agency_enabled?).to eq(true) - end - - it 'returns false if the agency is disabled' do - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('') - ai = AgencyIdentity.new(agency_id: 1, user_id: 1, uuid: 'UUID1') - expect(ai.agency_enabled?).to eq(false) - end - end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 13e7c12f2c6..550fe7b0291 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -60,10 +60,8 @@ end config.before(:each, idv_job: true) do - [Idv::ProfileJob, Idv::PhoneJob].each do |job_class| - allow(job_class).to receive(:perform_later) do |*args| - job_class.perform_now(*args) - end + allow(Idv::ProoferJob).to receive(:perform_later) do |*args| + Idv::ProoferJob.perform_now(*args) end end diff --git a/spec/services/agency_identity_linker_spec.rb b/spec/services/agency_identity_linker_spec.rb index a10950b7a15..1f99627e550 100644 --- a/spec/services/agency_identity_linker_spec.rb +++ b/spec/services/agency_identity_linker_spec.rb @@ -106,8 +106,6 @@ end def init_env(user) - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('true') - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1,2,3') Identity.where(user_id: user.id).delete_all AgencyIdentity.where(user_id: user.id).delete_all end diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb new file mode 100644 index 00000000000..65f20593abb --- /dev/null +++ b/spec/services/idv/agent_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' +require 'ostruct' + +describe Idv::Agent do + describe '.proofer_attribute?' do + it 'returns whether the attribute is available in Idv::Proofer::ATTRIBUTES' do + key = :foobarbaz + expect(Idv::Proofer).to receive(:attribute?).with(key) + Idv::Agent.proofer_attribute?(key) + end + end + + describe 'instance' do + let(:applicant) { { foo: 'bar' } } + + let(:agent) { Idv::Agent.new(applicant) } + + describe '#merge_results' do + let(:orig_results) do + { + errors: { foo: 'bar', bar: 'baz' }, + messages: ['reason 1'], + success: true, + exception: StandardError.new, + } + end + + let(:new_result) do + { + errors: { foo: 'blarg', baz: 'foo' }, + messages: ['reason 2'], + success: false, + exception: StandardError.new, + } + end + + let(:merged_results) { agent.send(:merge_results, orig_results, new_result) } + + it 'keeps the last errors' do + expect(merged_results[:errors]).to eq(new_result[:errors]) + end + + it 'concatenates messages' do + expect(merged_results[:messages]).to eq(orig_results[:messages] + new_result[:messages]) + end + + it 'keeps the last success' do + expect(merged_results[:success]).to eq(false) + end + + it 'keeps the last exception' do + expect(merged_results[:exception]).to eq(new_result[:exception]) + end + end + + describe '#proof' do + let(:resolution_message) { 'reason 1' } + let(:state_id_message) { 'reason 2' } + let(:failed_message) { 'bah humbug' } + let(:error) { { bad: 'stuff' } } + + subject { agent.proof(*stages) } + + before do + allow(Idv::Proofer).to receive(:get_vendor) do |stage| + logic = case stage + when :resolution + proc { |_, r| r.add_message('reason 1') } + when :state_id + proc { |_, r| r.add_message('reason 2') } + when :failed + proc { |_, r| r.add_message('bah humbug').add_error(:bad, 'stuff') } + end + Class.new(Proofer::Base) do + attributes(:foo) + proof(&logic) + end + end + end + + context 'when all stages succeed' do + let(:stages) { %i[resolution state_id] } + + it 'results from all stages are included' do + expect(subject.to_h).to eq( + errors: {}, + messages: [resolution_message, state_id_message], + success: true, + exception: nil, + ) + end + end + + context 'when the fist stage fails' do + let(:stages) { %i[failed state_id] } + + it 'only the results from the first stage are included' do + expect(subject.to_h).to eq( + errors: { bad: ['stuff'] }, + messages: [failed_message], + success: false, + exception: nil, + ) + end + end + end + end +end diff --git a/spec/services/idv/job_spec.rb b/spec/services/idv/job_spec.rb new file mode 100644 index 00000000000..bfb37ac5690 --- /dev/null +++ b/spec/services/idv/job_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe Idv::Job do + let(:idv_session) do + Idv::Session. + new(current_user: build(:user), issuer: nil, user_session: {}). + tap { |session| session.params = applicant } + end + + let(:applicant) { { first_name: 'Greatest', dob: '01/01/1985' } } + let(:result_id) { 'abcdef' } + let(:stages) { %i[resolution] } + + describe '#submit' do + it 'generates a UUID and enqueues a Idv::ProoferJob and saves the UUID in the session' do + expect(Idv::ProoferJob).to receive(:perform_later). + with( + result_id: result_id, + applicant_json: idv_session.vendor_params.to_json, + stages: stages.to_json + ) + + expect(idv_session.async_result_id).to eq(nil) + expect(idv_session.async_result_started_at).to eq(nil) + + expect(SecureRandom).to receive(:uuid).and_return(result_id).once + + Idv::Job.submit(idv_session, stages) + + expect(idv_session.async_result_id).to eq(result_id) + expect(idv_session.async_result_started_at).to be_within(1).of(Time.zone.now.to_i) + end + end +end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 65e084ad2e0..abe81f35ffe 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -6,7 +6,7 @@ let(:user) { build(:user) } let(:idv_session) do idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - idvs.applicant = Proofer::Applicant.new first_name: 'Some' + idvs.applicant = { first_name: 'Some' } idvs end let(:idv_form_params) { { phone: '555-555-0000', phone_confirmed_at: nil } } diff --git a/spec/services/idv/profile_maker_spec.rb b/spec/services/idv/profile_maker_spec.rb index 9fe23b1eae3..7775f528eae 100644 --- a/spec/services/idv/profile_maker_spec.rb +++ b/spec/services/idv/profile_maker_spec.rb @@ -3,8 +3,8 @@ describe Idv::ProfileMaker do describe '#save_profile' do it 'creates Profile with encrypted PII' do - applicant = Proofer::Applicant.new first_name: 'Some', last_name: 'One' - normalized_applicant = Proofer::Applicant.new first_name: 'Somebody', last_name: 'Oneatatime' + applicant = { first_name: 'Some', last_name: 'One' } + normalized_applicant = { first_name: 'Somebody', last_name: 'Oneatatime' } user = create(:user, :signed_up) user.unlock_user_access_key(user.password) diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index cc7655d0599..3500e685012 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -31,10 +31,10 @@ def build_step(params, vendor_validator_result) describe '#submit' do it 'succeeds with good params' do - reasons = ['Everything looks good'] + messages = ['Everything looks good'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: reasons }, + vendor: { messages: messages }, } step = build_step( @@ -42,8 +42,8 @@ def build_step(params, vendor_validator_result) Idv::VendorResult.new( success: true, errors: {}, - reasons: reasons, - normalized_applicant: Proofer::Applicant.new(first_name: 'Some') + messages: messages, + normalized_applicant: { first_name: 'Some' } ) ) @@ -57,16 +57,16 @@ def build_step(params, vendor_validator_result) end it 'fails with invalid SSN' do - reasons = ['The SSN was suspicious'] + messages = ['The SSN was suspicious'] errors = { ssn: ['Unverified SSN.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: reasons }, + vendor: { messages: messages }, } step = build_step( user_attrs.merge(ssn: '666-66-6666'), - Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + Idv::VendorResult.new(success: false, errors: errors, messages: messages) ) result = step.submit @@ -80,15 +80,15 @@ def build_step(params, vendor_validator_result) it 'fails with invalid first name' do errors = { first_name: ['Unverified first name.'] } - reasons = ['The name was suspicious'] + messages = ['The name was suspicious'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: reasons }, + vendor: { messages: messages }, } step = build_step( user_attrs.merge(first_name: 'Bad'), - Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + Idv::VendorResult.new(success: false, errors: errors, messages: messages) ) result = step.submit @@ -101,16 +101,16 @@ def build_step(params, vendor_validator_result) end it 'fails with invalid ZIP code on current address' do - reasons = ['The ZIP code was suspicious'] + messages = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: reasons }, + vendor: { messages: messages }, } step = build_step( user_attrs.merge(zipcode: '00000'), - Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + Idv::VendorResult.new(success: false, errors: errors, messages: messages) ) result = step.submit @@ -123,16 +123,16 @@ def build_step(params, vendor_validator_result) end it 'fails with invalid ZIP code on previous address' do - reasons = ['The ZIP code was suspicious'] + messages = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: reasons }, + vendor: { messages: messages }, } step = build_step( user_attrs.merge(prev_zipcode: '00000'), - Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + Idv::VendorResult.new(success: false, errors: errors, messages: messages) ) result = step.submit diff --git a/spec/services/idv/proofer_spec.rb b/spec/services/idv/proofer_spec.rb new file mode 100644 index 00000000000..c1e73dbfb7d --- /dev/null +++ b/spec/services/idv/proofer_spec.rb @@ -0,0 +1,261 @@ +require 'rails_helper' + +describe Idv::Proofer do + describe '.attribute?' do + subject { described_class.attribute?(attribute) } + + context 'when the attribute exists' do + context 'and is passed as a string' do + let(:attribute) { 'last_name' } + + it { is_expected.to eq(true) } + end + + context 'and is passed as a symbol' do + let(:attribute) { :last_name } + + it { is_expected.to eq(true) } + end + end + + context 'when the attribute does not exist' do + context 'and is passed as a string' do + let(:attribute) { 'fooobar' } + + it { is_expected.to eq(false) } + end + + context 'and is passed as a symbol' do + let(:attribute) { :fooobar } + + it { is_expected.to eq(false) } + end + end + end + + describe '.loaded_vendors' do + subject { described_class.send(:loaded_vendors) } + + it 'returns all of the subclasses of Proofer::Base' do + subclasses = ['foo'] + expect(::Proofer::Base).to receive(:subclasses).and_return(subclasses) + expect(subject).to eq(subclasses) + end + end + + describe '.available_vendors' do + subject { described_class.send(:available_vendors, configured_vendors, vendors) } + + let(:vendors) do + [ + class_double('Proofer::Base', vendor_name: 'foo'), + class_double('Proofer::Base', vendor_name: 'baz'), + ] + end + + let(:configured_vendors) { %w[foo bar] } + + it 'selects only the vendors that have been configured' do + available_vendors = [vendors.first] + expect(subject).to eq(available_vendors) + end + end + + describe '.require_mock_vendors' do + subject { described_class.send(:require_mock_vendors) } + + it 'requires all of the mock vendors' do + Dir[Rails.root.join('lib', 'proofer_mocks', '*')].each do |file| + expect_any_instance_of(Object).to receive(:require).with(file) + end + + subject + end + end + + describe '.assign_vendors' do + subject { described_class.send(:assign_vendors, stages, external_vendors, mock_vendors) } + + let(:stages) { %i[resolution state_id address] } + + let(:external_vendors) do + [ + class_double('Proofer::Base', supported_stage: :resolution), + class_double('Proofer::Base', supported_stage: :foo), + ] + end + + let(:mock_vendors) do + [ + class_double('Proofer::Base', supported_stage: :resolution), + class_double('Proofer::Base', supported_stage: 'state_id'), + class_double('Proofer::Base', supported_stage: :baz), + ] + end + + it 'maps stages to vendors, falling back to mock vendors' do + assigned_vendors = { + resolution: external_vendors.first, + state_id: mock_vendors.second, + } + expect(subject).to eq(assigned_vendors) + end + end + + describe '.stage_vendor' do + subject { described_class.send(:stage_vendor, stage, vendors) } + + let(:stage) { :foo } + + context 'when supported_stage is a string' do + let(:vendors) do + [ + class_double('Proofer::Base', supported_stage: :resolution), + class_double('Proofer::Base', supported_stage: 'foo'), + ] + end + + it 'selects the vendor for the stage' do + expect(subject).to eq(vendors.second) + end + end + + context 'when supported_stage is a symbol' do + let(:vendors) do + [ + class_double('Proofer::Base', supported_stage: :resolution), + class_double('Proofer::Base', supported_stage: :foo), + ] + end + + it 'selects the vendor for the stage' do + expect(subject).to eq(vendors.second) + end + end + + context 'when no vendor exists' do + let(:vendors) do + [ + class_double('Proofer::Base', supported_stage: :resolution), + ] + end + + it 'is nil' do + expect(subject).to be_nil + end + end + end + + describe '.validate_vendors' do + subject { described_class.send(:validate_vendors, stages, vendors) } + + let(:stages) { %i[foo] } + + context 'when there are vendors for all stages' do + let(:vendors) { { foo: class_double('Proofer::Base') } } + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + end + + context 'when there are stages without vendors' do + let(:vendors) { { bar: class_double('Proofer::Base') } } + + it 'does raises an error' do + expect { subject }.to raise_error("No proofer vendor configured for stage(s): foo") + end + end + end + + describe '.configure_vendors' do + subject { described_class.configure_vendors(stages, config) } + + let(:stages) { %i[foo] } + + let(:config) { double } + + let(:configured_vendors) { %w[vendor1 vendor2] } + + let(:loaded_vendors) do + [ + class_double('Proofer::Base', supported_stage: :foo, vendor_name: 'vendor3'), + class_double('Proofer::Base', supported_stage: :foo, vendor_name: 'vendor1'), + class_double('Proofer::Base', supported_stage: :bar, vendor_name: 'vendor2'), + ] + end + + let(:mock_vendors) do + [ + class_double('Proofer::Base', supported_stage: :foo), + class_double('Proofer::Base', supported_stage: :baz), + ] + end + + before do + expect(config).to receive(:vendors).and_return(configured_vendors) + end + + context 'default configuration' do + before do + expect(config).to receive(:mock_fallback).and_return(false) + expect(config).to receive(:raise_on_missing_proofers).and_return(true) + expect(described_class).to receive(:loaded_vendors).and_return(loaded_vendors, loaded_vendors) + end + + context 'when a stage is missing an external vendor' do + let(:stages) { %i[foo baz] } + + it 'raises' do + expect { subject }.to raise_error("No proofer vendor configured for stage(s): baz") + end + end + + context 'when all stages have vendors' do + it 'maps the vendors, ignoring non-configured ones' do + expect(subject).to eq({ foo: loaded_vendors.second }) + end + end + end + + context 'when mock_fallback is enabled' do + before do + expect(config).to receive(:mock_fallback).and_return(true) + expect(config).to receive(:raise_on_missing_proofers).and_return(true) + expect(described_class).to receive(:loaded_vendors).and_return(loaded_vendors, mock_vendors) + end + + context 'when a stage is missing an external vendor' do + let(:stages) { %i[foo baz] } + + it 'does not raise' do + expect { subject }.not_to raise_error + end + + it 'returns the mapped vendors with the mock fallback' do + expect(subject).to eq({ foo: loaded_vendors.second, baz: mock_vendors.second }) + end + end + end + + context 'when raise_on_missing_proofers is disabled' do + before do + expect(config).to receive(:mock_fallback).and_return(false) + expect(config).to receive(:raise_on_missing_proofers).and_return(false) + expect(described_class).to receive(:loaded_vendors).and_return(loaded_vendors, loaded_vendors) + end + + context 'when a stage is missing an external vendor' do + let(:stages) { %i[foo baz] } + + it 'does not raise' do + expect { subject }.not_to raise_error + end + + it 'returns the mapped vendors missing the stage' do + expect(subject).to eq({ foo: loaded_vendors.second }) + end + end + end + end +end diff --git a/spec/services/idv/submit_idv_job_spec.rb b/spec/services/idv/submit_idv_job_spec.rb deleted file mode 100644 index 7d4530bc8d3..00000000000 --- a/spec/services/idv/submit_idv_job_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'rails_helper' - -RSpec.describe Idv::SubmitIdvJob do - subject(:service) do - Idv::SubmitIdvJob.new( - idv_session: idv_session, - vendor_params: vendor_params - ) - end - - let(:idv_session) do - Idv::Session.new( - current_user: user, - issuer: nil, - user_session: { - idv: { - applicant: applicant, - }, - } - ) - end - - let(:user) { build(:user) } - let(:applicant) { Proofer::Applicant.new(first_name: 'Greatest') } - let(:result_id) { 'abcdef' } - let(:vendor_params) { { dob: '01/01/1985' } } - - describe '#submit_profile_job' do - it 'generates a UUID and enqueues a Idv::ProfileJob and saves the UUID in the session' do - expect(Idv::ProfileJob).to receive(:perform_later). - with( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant.to_json - ) - - expect(idv_session.async_result_id).to eq(nil) - expect(idv_session.async_result_started_at).to eq(nil) - - expect(SecureRandom).to receive(:uuid).and_return(result_id).once - - service.submit_profile_job - - expect(idv_session.async_result_id).to eq(result_id) - expect(idv_session.async_result_started_at).to be_within(1).of(Time.zone.now.to_i) - end - end - - describe '#submit_phone_job' do - let(:vendor_params) { '5555550000' } - - it 'generates a UUID and enqueues a Idv::PhoneJob and saves the UUID in the session' do - expect(Idv::PhoneJob).to receive(:perform_later). - with( - result_id: result_id, - vendor_params: vendor_params, - applicant_json: applicant.to_json - ) - - expect(idv_session.async_result_id).to eq(nil) - expect(idv_session.async_result_started_at).to eq(nil) - - expect(SecureRandom).to receive(:uuid).and_return(result_id).once - - service.submit_phone_job - - expect(idv_session.async_result_id).to eq(result_id) - expect(idv_session.async_result_started_at).to be_within(1).of(Time.zone.now.to_i) - end - end -end diff --git a/spec/services/idv/upcase_vendor_env_vars_spec.rb b/spec/services/idv/upcase_vendor_env_vars_spec.rb deleted file mode 100644 index 30912329e0c..00000000000 --- a/spec/services/idv/upcase_vendor_env_vars_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'rails_helper' - -describe Idv::UpcaseVendorEnvVars do - describe '#call' do - before do - allow(Figaro.env).to receive(:profile_proofing_vendor).and_return('equifax') - allow(Figaro.env).to receive(:state_id_proofing_vendor).and_return('aamva') - allow(Figaro.env).to receive(:phone_proofing_vendor).and_return('equifax') - stub_const 'ENV', ENV.to_h.merge( - 'equifax_thing' => 'some value', - 'aamva_thing' => 'other value' - ) - end - - it 'sets UPPER case value equal to existing ENV var' do - expect(ENV['FOO_THING']).to eq nil - - subject.call - - expect(ENV['equifax_thing']).to eq 'some value' - expect(ENV['EQUIFAX_THING']).to eq 'some value' - expect(ENV['aamva_thing']).to eq 'other value' - expect(ENV['AAMVA_THING']).to eq 'other value' - end - end -end diff --git a/spec/services/idv/vendor_result_spec.rb b/spec/services/idv/vendor_result_spec.rb index 7d31b33dea1..e19651dccc5 100644 --- a/spec/services/idv/vendor_result_spec.rb +++ b/spec/services/idv/vendor_result_spec.rb @@ -3,20 +3,15 @@ RSpec.describe Idv::VendorResult do let(:success) { true } let(:errors) { { foo: ['is not valid'] } } - let(:reasons) { %w[foo bar baz] } - let(:normalized_applicant) do - Proofer::Applicant.new( - last_name: 'Ever', - first_name: 'Greatest' - ) - end + let(:messages) { %w[foo bar baz] } + let(:normalized_applicant) { { last_name: 'Ever', first_name: 'Greatest' } } let(:timed_out) { false } subject(:vendor_result) do Idv::VendorResult.new( success: success, errors: errors, - reasons: reasons, + messages: messages, normalized_applicant: normalized_applicant, timed_out: timed_out ) @@ -39,7 +34,7 @@ json = vendor_result.to_json parsed = JSON.parse(json, symbolize_names: true) - expect(parsed[:normalized_applicant][:last_name]).to eq(normalized_applicant.last_name) + expect(parsed[:normalized_applicant][:last_name]).to eq(normalized_applicant[:last_name]) end end @@ -49,11 +44,11 @@ it 'has simple attributes' do expect(new_from_json.success?).to eq(vendor_result.success?) expect(new_from_json.errors).to eq(vendor_result.errors) - expect(new_from_json.reasons).to eq(vendor_result.reasons) + expect(new_from_json.messages).to eq(vendor_result.messages) end it 'turns applicant into a full object' do - expect(new_from_json.normalized_applicant.last_name).to eq(normalized_applicant.last_name) + expect(new_from_json.normalized_applicant[:last_name]).to eq(normalized_applicant[:last_name]) end context 'without an applicant' do diff --git a/spec/services/link_agency_identities_spec.rb b/spec/services/link_agency_identities_spec.rb index 4b5b928d40b..0567bc90c76 100644 --- a/spec/services/link_agency_identities_spec.rb +++ b/spec/services/link_agency_identities_spec.rb @@ -78,8 +78,6 @@ end def init_env(user) - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('true') - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1,2,3') AgencySeeder.new(rails_env: Rails.env, deploy_env: Rails.env).run Identity.where(user_id: user.id).delete_all AgencyIdentity.where(user_id: user.id).delete_all diff --git a/spec/services/marketing_site_spec.rb b/spec/services/marketing_site_spec.rb index 80127eac095..b7d60fa8d74 100644 --- a/spec/services/marketing_site_spec.rb +++ b/spec/services/marketing_site_spec.rb @@ -57,19 +57,19 @@ end end - describe '.help_authenticator_app_url' do - it 'points to the authenticator app section of the help page' do - expect(MarketingSite.help_authenticator_app_url).to eq( - 'https://www.login.gov/help/signing-in/what-is-an-authenticator-app/' + describe '.help_authentication_app_url' do + it 'points to the authentication app section of the help page' do + expect(MarketingSite.help_authentication_app_url).to eq( + 'https://www.login.gov/help/signing-in/what-is-an-authentication-app/' ) end context 'when the user has set their locale to :es' do before { I18n.locale = :es } - it 'points to the authenticator app section of the help page with the locale appended' do - expect(MarketingSite.help_authenticator_app_url).to eq( - 'https://www.login.gov/es/help/signing-in/what-is-an-authenticator-app/' + it 'points to the authentication app section of the help page with the locale appended' do + expect(MarketingSite.help_authentication_app_url).to eq( + 'https://www.login.gov/es/help/signing-in/what-is-an-authentication-app/' ) end end diff --git a/spec/services/vendor_validator_result_storage_spec.rb b/spec/services/vendor_validator_result_storage_spec.rb index 3ba45b313a9..63505b53f0e 100644 --- a/spec/services/vendor_validator_result_storage_spec.rb +++ b/spec/services/vendor_validator_result_storage_spec.rb @@ -7,7 +7,7 @@ let(:original_result) do Idv::VendorResult.new( success: false, - normalized_applicant: Proofer::Applicant.new(first_name: 'First') + normalized_applicant: { first_name: 'First' } ) end @@ -35,7 +35,7 @@ expect(result.success?).to eq(original_result.success?) expect(result.errors).to eq(original_result.errors) - expect(result.reasons).to eq(original_result.reasons) + expect(result.messages).to eq(original_result.messages) expect(result.normalized_applicant.as_json). to eq(original_result.normalized_applicant.as_json) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 320d249d6a6..f331654de53 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,7 @@ add_filter '/config/' add_filter '/lib/rspec/formatters/user_flow_formatter.rb' add_filter '/lib/user_flow_exporter.rb' + add_filter '/lib/deploy/migration_statement_timeout.rb' end end diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index 0d111dddfd5..88544ad25b7 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -36,7 +36,7 @@ def stub_verify_steps_one_and_two(user) user_session = {} stub_sign_in(user) idv_session = Idv::Session.new(user_session: user_session, current_user: user, issuer: nil) - idv_session.applicant = Proofer::Applicant.new first_name: 'Some', last_name: 'One' + idv_session.applicant = { first_name: 'Some', last_name: 'One' } allow(subject).to receive(:confirm_idv_session_started).and_return(true) allow(subject).to receive(:confirm_idv_attempts_allowed).and_return(true) allow(subject).to receive(:idv_session).and_return(idv_session) diff --git a/spec/support/idv/proofer_job_helper.rb b/spec/support/idv/proofer_job_helper.rb deleted file mode 100644 index 9005a160d1c..00000000000 --- a/spec/support/idv/proofer_job_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -module ProoferJobHelper - def mock_proofer_job_agent(config:, vendor:) - allow(Figaro.env).to receive(config).and_return(vendor) - - agent = Idv::Agent.new(applicant: Proofer::Applicant.new({}), vendor: :mock) - allow(Idv::Agent).to receive(:new).and_return(agent) - end -end diff --git a/spec/support/idv_examples/failed_idv_job.rb b/spec/support/idv_examples/failed_idv_job.rb index dae5c935c0c..f7d8a02d6c3 100644 --- a/spec/support/idv_examples/failed_idv_job.rb +++ b/spec/support/idv_examples/failed_idv_job.rb @@ -1,8 +1,5 @@ shared_examples 'failed idv job' do |step| - let(:idv_job_class) do - return Idv::ProfileJob if step == :profile - return Idv::PhoneJob if step == :phone - end + let(:idv_job_class) { Idv::ProoferJob } let(:step_locale_key) do return :sessions if step == :profile step @@ -85,12 +82,7 @@ end def stub_idv_job_to_raise_error_in_background(idv_job_class) - allow(idv_job_class).to receive(:new).and_wrap_original do |new, *args| - idv_job = new.call(*args) - allow(idv_job).to receive(:verify_identity_with_vendor). - and_raise('this is a test error') - idv_job - end + allow(Idv::Agent).to receive(:new).and_raise('this is a test error') allow(idv_job_class).to receive(:perform_now).and_wrap_original do |perform_now, *args| begin perform_now.call(*args) diff --git a/spec/support/idv_examples/otp_delivery_method.rb b/spec/support/idv_examples/otp_delivery_method.rb deleted file mode 100644 index 246fe0aa446..00000000000 --- a/spec/support/idv_examples/otp_delivery_method.rb +++ /dev/null @@ -1,65 +0,0 @@ -shared_examples 'idv otp delivery method selection' do |sp| - let(:phone) { '555-123-4567' } - - before do - visit_idp_from_sp_with_loa3(sp) - register_user - click_idv_begin - fill_out_idv_form_ok - click_idv_continue - click_idv_address_choose_phone - fill_out_phone_form_ok(phone) - click_idv_continue - end - - scenario 'selecting sms delivery method sends sems', :email do - allow(SmsOtpSenderJob).to receive(:perform_later) - choose_idv_otp_delivery_method_sms - - expect(SmsOtpSenderJob).to have_received(:perform_later) - expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :sms) - end - - scenario 'selecting voice delivery method sends voice call', :email do - allow(VoiceOtpSenderJob).to receive(:perform_later) - choose_idv_otp_delivery_method_voice - - expect(VoiceOtpSenderJob).to have_received(:perform_later) - expect(current_path).to eq login_two_factor_path(otp_delivery_preference: :voice) - end - - scenario 'choosing to enter a different phone sends an OTP to that phone', :email do - different_phone = '9876543210' - - choose_idv_otp_delivery_method_sms - click_link t('forms.two_factor.try_again') - - expect(current_path).to eq verify_phone_path - - fill_out_phone_form_ok(different_phone) - click_idv_continue - - allow(SmsOtpSenderJob).to receive(:perform_later) - choose_idv_otp_delivery_method_sms - - expect(SmsOtpSenderJob).to have_received(:perform_later). - with(hash_including(phone: different_phone)) - end - - context 'with a phone number that does not support voice calling' do - let(:phone) { '242-555-5000' } - - scenario 'voice call option is disabled', :email do - voice_radio_button = page.find( - '#otp_delivery_selection_form_otp_delivery_preference_voice', - visible: false - ) - - expect(voice_radio_button.disabled?).to eq(true) - expect(page).to have_content t( - 'devise.two_factor_authentication.otp_delivery_preference.phone_unsupported', - location: 'Bahamas' - ) - end - end -end diff --git a/spec/support/idv_examples/sp_handoff.rb b/spec/support/idv_examples/sp_handoff.rb index b8f5d608796..de1dca459ce 100644 --- a/spec/support/idv_examples/sp_handoff.rb +++ b/spec/support/idv_examples/sp_handoff.rb @@ -3,8 +3,6 @@ include IdvHelper before do - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('true') - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1,2,3') allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) end diff --git a/spec/support/idv_examples/sp_requested_attributes.rb b/spec/support/idv_examples/sp_requested_attributes.rb index 2c4710edb17..230ccd59396 100644 --- a/spec/support/idv_examples/sp_requested_attributes.rb +++ b/spec/support/idv_examples/sp_requested_attributes.rb @@ -3,8 +3,6 @@ include IdvHelper before do - allow(Figaro.env).to receive(:enable_agency_based_uuids).and_return('true') - allow(Figaro.env).to receive(:agencies_with_agency_based_uuids).and_return('1,2,3') allow(FeatureManagement).to receive(:prefill_otp_codes?).and_return(true) end diff --git a/spec/support/idv_examples/usps_verification_selection.rb b/spec/support/idv_examples/usps_verification_selection.rb deleted file mode 100644 index 7c4031081cf..00000000000 --- a/spec/support/idv_examples/usps_verification_selection.rb +++ /dev/null @@ -1,50 +0,0 @@ -shared_examples 'selecting usps address verification method' do |sp| - it 'allows the user to select verification via USPS letter', email: true do - visit_idp_from_sp_with_loa3(sp) - - user = register_user - - click_idv_begin - fill_out_idv_form_ok - click_idv_continue - - click_idv_address_choose_usps - click_on t('idv.buttons.mail.send') - - expect(current_path).to eq verify_review_path - expect(page).to_not have_content t('idv.messages.phone.phone_of_record') - - fill_in :user_password, with: user_password - - expect { click_continue }. - to change { UspsConfirmation.count }.from(0).to(1) - - expect(current_path).to eq verify_confirmations_path - click_acknowledge_personal_key - - user.reload - - expect(user.events.account_verified.size).to be(0) - expect(user.profiles.count).to eq 1 - - profile = user.profiles.first - - expect(profile.active?).to eq false - expect(profile.deactivation_reason).to eq 'verification_pending' - expect(profile.phone_confirmed).to eq false - - usps_confirmation_entry = UspsConfirmation.last.decrypted_entry - - expect(current_path).to eq(verify_come_back_later_path) - - if sp == :saml - expect(page).to have_link(t('idv.buttons.continue_plain')) - expect(usps_confirmation_entry.issuer). - to eq('https://rp1.serviceprovider.com/auth/saml/metadata') - elsif sp == :oidc - expect(page).to have_link(t('idv.buttons.continue_plain')) - expect(usps_confirmation_entry.issuer). - to eq('urn:gov:gsa:openidconnect:sp:server') - end - end -end diff --git a/spec/support/shared_examples/remember_device.rb b/spec/support/shared_examples/remember_device.rb index 8d1a65da850..d45f13b64e3 100644 --- a/spec/support/shared_examples/remember_device.rb +++ b/spec/support/shared_examples/remember_device.rb @@ -19,6 +19,9 @@ it 'requires 2FA on sign in after phone number is changed' do user = remember_device_and_sign_out_user + # Ensure that at least 1 second has passed since last `remember device` + sleep(1) + sign_in_user(user) visit manage_phone_path fill_in 'user_phone_form_phone', with: '5551230000'