diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index afe4b702609..db9eab7cdb4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -63,7 +63,7 @@ stages:
- after_test
- review
- scan
- - deploy_production
+ - deploy_eks
workflow:
rules:
@@ -604,21 +604,6 @@ stop-review-app:
- if: $CI_PIPELINE_SOURCE != "merge_request_event"
when: never
-deploy_production:
- stage: deploy_production
- allow_failure: true
- needs:
- - job: build-review-image
- resource_group: $CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov
- extends: .deploy
- environment:
- name: production
- deployment_tier: production
- url: https://$CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov
- rules:
- - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
-
-
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
@@ -867,3 +852,15 @@ audit_packages_scheduled:
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
+
+# EKS deployment
+deploy_eks:
+ trigger:
+ project: lg-public/identity-eks-control
+ branch: main
+ stage: deploy_eks
+ variables:
+ APP: idp
+ IMAGE_TAG: $CI_COMMIT_SHA
+ rules:
+ - if: $CI_COMMIT_BRANCH == "main"
diff --git a/app/helpers/ipp_helper.rb b/app/helpers/ipp_helper.rb
index a08e3394e5e..24c3533d2e1 100644
--- a/app/helpers/ipp_helper.rb
+++ b/app/helpers/ipp_helper.rb
@@ -8,8 +8,14 @@ def scrub_message(message)
def scrub_body(body)
return nil if body.nil?
- body = body.with_indifferent_access
- body[:responseMessage] = scrub_message(body[:responseMessage])
- body
+ if body.is_a?(String)
+ scrub_message(body)
+ else
+ body = body.with_indifferent_access
+ if body[:responseMessage].present?
+ body[:responseMessage] = scrub_message(body[:responseMessage])
+ end
+ body
+ end
end
end
diff --git a/app/jobs/reports/duplicate_ssn_report.rb b/app/jobs/reports/duplicate_ssn_report.rb
index 2e727d77e16..bbb1da3be1b 100644
--- a/app/jobs/reports/duplicate_ssn_report.rb
+++ b/app/jobs/reports/duplicate_ssn_report.rb
@@ -38,10 +38,12 @@ def report_body
ssn_signatures = todays_profiles.map(&:ssn_signature).uniq
- profiles_connected_by_ssn = Profile.
- includes(:user).
- where(ssn_signature: ssn_signatures).
- to_a
+ profiles_connected_by_ssn = ssn_signatures.each_slice(1000).flat_map do |ssn_signature_slice|
+ Profile.
+ includes(:user).
+ where(ssn_signature: ssn_signature_slice).
+ to_a
+ end
profiles_connected_by_ssn.sort_by!(&:id).reverse!
diff --git a/app/jobs/reports/idv_legacy_conversion_supplement_report.rb b/app/jobs/reports/idv_legacy_conversion_supplement_report.rb
new file mode 100644
index 00000000000..dbc8c95dab0
--- /dev/null
+++ b/app/jobs/reports/idv_legacy_conversion_supplement_report.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'csv'
+
+module Reports
+ class IdvLegacyConversionSupplementReport < BaseReport
+ REPORT_NAME = 'idv-legacy-conversion-supplement-report'
+
+ def perform(_date)
+ csv = build_csv
+ save_report(REPORT_NAME, csv, extension: 'csv')
+ end
+
+ # @return [String] CSV report
+ def build_csv
+ sql = <<~SQL
+ SELECT
+ iaa_orders.start_date
+ , iaa_orders.end_date
+ , iaa_orders.order_number
+ , iaa_gtcs.gtc_number AS gtc_number
+ , upgrade.issuer AS issuer
+ , sp.friendly_name AS friendly_name
+ , DATE_TRUNC('month', upgrade.upgraded_at) AS year_month
+ , COUNT(DISTINCT upgrade.user_id) AS user_count
+ FROM iaa_orders
+ INNER JOIN integration_usages iu ON iu.iaa_order_id = iaa_orders.id
+ INNER JOIN integrations ON integrations.id = iu.integration_id
+ INNER JOIN iaa_gtcs ON iaa_gtcs.id = iaa_orders.iaa_gtc_id
+ INNER JOIN service_providers sp ON sp.issuer = integrations.issuer
+ INNER JOIN (
+ SELECT DISTINCT ON (user_id) *
+ FROM sp_upgraded_biometric_profiles
+ ) upgrade ON upgrade.issuer = integrations.issuer
+ WHERE upgrade.upgraded_at BETWEEN iaa_orders.start_date AND iaa_orders.end_date
+ GROUP BY iaa_orders.id, upgrade.issuer, year_month, iaa_gtcs.gtc_number, sp.friendly_name
+ ORDER BY iaa_orders.id, year_month
+ SQL
+
+ results = transaction_with_timeout do
+ ActiveRecord::Base.connection.select_all(sql)
+ end
+
+ CSV.generate do |csv|
+ csv << [
+ 'iaa_order_number',
+ 'iaa_start_date',
+ 'iaa_end_date',
+ 'issuer',
+ 'friendly_name',
+ 'year_month',
+ 'year_month_readable',
+ 'user_count',
+ ]
+
+ results.each do |iaa|
+ csv << [
+ IaaReportingHelper.key(
+ gtc_number: iaa['gtc_number'],
+ order_number: iaa['order_number'],
+ ),
+ iaa['start_date'],
+ iaa['end_date'],
+ iaa['issuer'],
+ iaa['friendly_name'],
+ iaa['year_month'].strftime('%Y%m'),
+ iaa['year_month'].strftime('%B %Y'),
+ iaa['user_count'],
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/profile.rb b/app/models/profile.rb
index e5c9671d4bf..12edabbee1e 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -92,7 +92,9 @@ def activate(reason_deactivated: nil)
confirm_that_profile_can_be_activated!
now = Time.zone.now
- is_reproof = Profile.find_by(user_id: user_id, active: true)
+ profile_to_deactivate = Profile.find_by(user_id: user_id, active: true)
+ is_reproof = profile_to_deactivate.present?
+ is_biometric_upgrade = is_reproof && biometric? && !profile_to_deactivate.biometric?
attrs = {
active: true,
@@ -105,6 +107,7 @@ def activate(reason_deactivated: nil)
Profile.where(user_id: user_id).update_all(active: false)
update!(attrs)
end
+ track_biometric_reproof if is_biometric_upgrade
send_push_notifications if is_reproof
end
# rubocop:enable Rails/SkipsModelValidations
@@ -199,8 +202,8 @@ def deactivate_due_to_gpo_expiration
def deactivate_due_to_in_person_verification_cancelled
update!(
active: false,
- deactivation_reason: :verification_cancelled,
in_person_verification_pending_at: nil,
+ deactivation_reason: deactivation_reason.presence || :verification_cancelled,
)
end
@@ -306,6 +309,10 @@ def profile_age_in_seconds
(Time.zone.now - created_at).round
end
+ def biometric?
+ ::User::BIOMETRIC_COMPARISON_IDV_LEVELS.include?(idv_level)
+ end
+
private
def confirm_that_profile_can_be_activated!
@@ -333,4 +340,13 @@ def send_push_notifications
event = PushNotification::ReproofCompletedEvent.new(user: user)
PushNotification::HttpPush.deliver(event)
end
+
+ def track_biometric_reproof
+ SpUpgradedBiometricProfile.create(
+ user: user,
+ upgraded_at: Time.zone.now,
+ idv_level: idv_level,
+ issuer: initiating_service_provider_issuer,
+ )
+ end
end
diff --git a/app/models/sp_upgraded_biometric_profile.rb b/app/models/sp_upgraded_biometric_profile.rb
new file mode 100644
index 00000000000..d3a483d8bfd
--- /dev/null
+++ b/app/models/sp_upgraded_biometric_profile.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class SpUpgradedBiometricProfile < ApplicationRecord
+ belongs_to :user
+end
diff --git a/app/services/iaa_reporting_helper.rb b/app/services/iaa_reporting_helper.rb
index 8c25a892c7d..2422c1d71df 100644
--- a/app/services/iaa_reporting_helper.rb
+++ b/app/services/iaa_reporting_helper.rb
@@ -13,7 +13,7 @@ module IaaReportingHelper
) do
# ex LG123567-0001
def key
- "#{gtc_number}-#{format('%04d', order_number)}"
+ IaaReportingHelper.key(gtc_number:, order_number:)
end
end
@@ -74,4 +74,8 @@ def partner_accounts
end
end.compact
end
+
+ def key(gtc_number:, order_number:)
+ "#{gtc_number}-#{format('%04d', order_number)}"
+ end
end
diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb
index 374a83572f1..b9b3bf1aee1 100644
--- a/app/services/proofing/aamva/proofer.rb
+++ b/app/services/proofing/aamva/proofer.rb
@@ -21,6 +21,13 @@ class Proofer
],
).freeze
+ REQUIRED_VERIFICATION_ATTRIBUTES = %i[
+ state_id_number
+ dob
+ last_name
+ first_name
+ ].freeze
+
ADDRESS_ATTRIBUTES = [
:address1,
:address2,
@@ -63,7 +70,7 @@ def proof(applicant)
def build_result_from_response(verification_response, jurisdiction)
Proofing::StateIdResult.new(
- success: verification_response.success?,
+ success: successful?(verification_response),
errors: parse_verification_errors(verification_response),
exception: nil,
vendor_name: 'aamva:state_id',
@@ -77,7 +84,7 @@ def build_result_from_response(verification_response, jurisdiction)
def parse_verification_errors(verification_response)
errors = Hash.new { |h, k| h[k] = [] }
- return errors if verification_response.success?
+ return errors if successful?(verification_response)
verification_response.verification_results.each do |attribute, v_result|
attribute_key = attribute.to_sym
@@ -121,6 +128,14 @@ def send_to_new_relic(result)
NewRelic::Agent.notice_error(result.exception)
end
+ def successful?(verification_response)
+ REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute|
+ return false unless verification_response.verification_results[verification_attribute]
+ end
+
+ true
+ end
+
def jurisdiction_in_maintenance_window?(state)
Idv::AamvaStateMaintenanceWindow.in_maintenance_window?(state)
end
diff --git a/app/services/proofing/aamva/response/verification_response.rb b/app/services/proofing/aamva/response/verification_response.rb
index adcbdadfb7b..c0fc81d40d9 100644
--- a/app/services/proofing/aamva/response/verification_response.rb
+++ b/app/services/proofing/aamva/response/verification_response.rb
@@ -22,13 +22,6 @@ class VerificationResponse
'AddressZIP5MatchIndicator' => :zipcode,
}.freeze
- REQUIRED_VERIFICATION_ATTRIBUTES = %i[
- state_id_number
- dob
- last_name
- first_name
- ].freeze
-
attr_reader :verification_results, :transaction_locator_id
def initialize(http_response)
@@ -48,26 +41,6 @@ def initialize(http_response)
raise VerificationError.new(error_message)
end
- def reasons
- REQUIRED_VERIFICATION_ATTRIBUTES.map do |verification_attribute|
- verification_result = verification_results[verification_attribute]
- case verification_result
- when false
- "Failed to verify #{verification_attribute}"
- when nil
- "Response was missing #{verification_attribute}"
- end
- end.compact
- end
-
- def success?
- REQUIRED_VERIFICATION_ATTRIBUTES.each do |verification_attribute|
- return false unless verification_results[verification_attribute]
- end
-
- true
- end
-
private
attr_reader :http_response, :missing_attributes
diff --git a/config/application.yml.default b/config/application.yml.default
index 63d0fd90c9b..92057c61299 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -62,10 +62,12 @@ component_previews_enabled: false
compromised_password_randomizer_threshold: 900
compromised_password_randomizer_value: 1000
country_phone_number_overrides: '{}'
+database_advisory_locks_enabled: false
database_host: ''
database_name: ''
database_password: ''
database_pool_idp: 5
+database_prepared_statements_enabled: false
database_read_replica_host: ''
database_readonly_password: ''
database_readonly_username: ''
diff --git a/config/database.yml b/config/database.yml
index 0b623f08000..05c4561eb15 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -86,8 +86,8 @@ production:
host: <%= IdentityConfig.store.database_socket.present? ? IdentityConfig.store.database_socket : IdentityConfig.store.database_host %>
password: <%= IdentityConfig.store.database_password %>
pool: <%= primary_pool %>
- advisory_locks: <%= !IdentityConfig.store.database_socket.present? %>
- prepared_statements: <%= !IdentityConfig.store.database_socket.present? %>
+ advisory_locks: <%= IdentityConfig.store.database_advisory_locks_enabled %>
+ prepared_statements: <%= IdentityConfig.store.database_prepared_statements_enabled %>
sslmode: <%= IdentityConfig.store.database_sslmode %>
sslrootcert: '/usr/local/share/aws/rds-combined-ca-bundle.pem'
migrations_paths: db/primary_migrate
diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb
index ac97907c86e..5559da9bb78 100644
--- a/config/initializers/job_configurations.rb
+++ b/config/initializers/job_configurations.rb
@@ -59,6 +59,12 @@
cron: cron_24h,
args: -> { [Time.zone.today] },
},
+ # Idv Legacy Conversion Supplement Report to S3
+ idv_legacy_conversion_supplement_report: {
+ class: 'Reports::IdvLegacyConversionSupplementReport',
+ cron: cron_24h,
+ args: -> { [Time.zone.today] },
+ },
agreement_summary_report: {
class: 'Reports::AgreementSummaryReport',
cron: cron_24h,
diff --git a/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb b/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb
new file mode 100644
index 00000000000..0c6c9699d0c
--- /dev/null
+++ b/db/primary_migrate/20240916202940_create_sp_upgraded_biometric_profiles.rb
@@ -0,0 +1,14 @@
+class CreateSpUpgradedBiometricProfiles < ActiveRecord::Migration[7.1]
+ def change
+ create_table :sp_upgraded_biometric_profiles do |t|
+ t.datetime :upgraded_at, null: false
+ t.references :user, null: false
+ t.string :idv_level, null: false
+ t.string :issuer, null: false
+
+ t.timestamps
+
+ t.index [:issuer, :upgraded_at]
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 385018287e3..cd2309f566f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_08_28_182041) do
+ActiveRecord::Schema[7.1].define(version: 2024_09_16_202940) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@@ -585,6 +585,17 @@
t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true
end
+ create_table "sp_upgraded_biometric_profiles", force: :cascade do |t|
+ t.datetime "upgraded_at", null: false
+ t.bigint "user_id", null: false
+ t.string "idv_level", null: false
+ t.string "issuer", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["issuer", "upgraded_at"], name: "index_sp_upgraded_biometric_profiles_on_issuer_and_upgraded_at"
+ t.index ["user_id"], name: "index_sp_upgraded_biometric_profiles_on_user_id"
+ end
+
create_table "suspended_emails", force: :cascade do |t|
t.bigint "email_address_id", null: false
t.string "digested_base_email", null: false
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 3dd9c8f1f98..67388dead1d 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -81,10 +81,12 @@ def self.store
config.add(:country_phone_number_overrides, type: :json)
config.add(:dashboard_api_token, type: :string)
config.add(:dashboard_url, type: :string)
+ config.add(:database_advisory_locks_enabled, type: :boolean)
config.add(:database_host, type: :string)
config.add(:database_name, type: :string)
config.add(:database_password, type: :string)
config.add(:database_pool_idp, type: :integer)
+ config.add(:database_prepared_statements_enabled, type: :boolean)
config.add(:database_read_replica_host, type: :string)
config.add(:database_readonly_password, type: :string)
config.add(:database_readonly_username, type: :string)
diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
index 8f0672fe808..10572bc74f8 100644
--- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
+++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
@@ -324,6 +324,35 @@
expect(status).to eq 422
end
end
+
+ context 'with 400 error without a response message' do
+ let(:response_message) { 'SponsorID 57 is not registered as an IPP client' }
+ let(:response_body) { { differentMessage: 'Something else is wrong' } }
+ let(:error_response) { { body: response_body, status: 400 } }
+ let(:sponsor_id_error) { Faraday::BadRequestError.new(response_message, error_response) }
+ let(:filtered_message) { 'sponsorID [FILTERED] is not registered as an IPP client' }
+
+ before do
+ allow(proofer).to receive(:request_facilities).and_raise(sponsor_id_error)
+ end
+
+ it 'returns an unprocessible entity client error with scrubbed analytics event' do
+ subject
+
+ expect(@analytics).to have_logged_event(
+ 'Request USPS IPP locations: request failed',
+ api_status_code: 422,
+ exception_class: sponsor_id_error.class,
+ exception_message: filtered_message,
+ response_body_present: true,
+ response_body: response_body,
+ response_status_code: 400,
+ )
+
+ status = response.status
+ expect(status).to eq 422
+ end
+ end
end
describe '#update' do
diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb
index c79e6d0d124..f39daca07a9 100644
--- a/spec/factories/profiles.rb
+++ b/spec/factories/profiles.rb
@@ -79,6 +79,11 @@
end
end
+ trait :biometric_proof do
+ idv_level { :in_person }
+ initiating_service_provider_issuer { 'urn:gov:gsa:openidconnect:inactive:sp:test' }
+ end
+
after(:build) do |profile, evaluator|
if evaluator.pii
pii_attrs = Pii::Attributes.new_from_hash(evaluator.pii)
diff --git a/spec/factories/sp_upgraded_biometric_profiles.rb b/spec/factories/sp_upgraded_biometric_profiles.rb
new file mode 100644
index 00000000000..faa18072397
--- /dev/null
+++ b/spec/factories/sp_upgraded_biometric_profiles.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :sp_upgraded_biometric_profile do
+ idv_level { :in_person }
+ end
+end
diff --git a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml
index 19d08f8624e..d875f2d12a4 100644
--- a/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml
+++ b/spec/fixtures/proofing/aamva/responses/verification_response_namespaced_success.xml
@@ -16,6 +16,7 @@
true
+ true
true
true
true
diff --git a/spec/helpers/ipp_helper_spec.rb b/spec/helpers/ipp_helper_spec.rb
new file mode 100644
index 00000000000..770690f653a
--- /dev/null
+++ b/spec/helpers/ipp_helper_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+RSpec.describe IppHelper do
+ include IppHelper
+
+ describe '#scrub_body' do
+ let(:message) { "This is a test message with sponsorID #{sponsor_id}" }
+ let(:sponsor_id) { 1111111 }
+
+ context 'when body is a String' do
+ it 'scrubs the sponsorID from the message' do
+ expect(scrub_body(message)).to eq('This is a test message with sponsorID [FILTERED]')
+ end
+ end
+
+ context 'when body is a Hash' do
+ it 'scrubs the responseMessage' do
+ body = { responseMessage: message }
+ expect(scrub_body(body)).to eq(
+ 'responseMessage' => 'This is a test message with sponsorID [FILTERED]',
+ )
+ end
+ end
+
+ context 'when body is nil' do
+ it 'returns nil' do
+ expect(scrub_body(nil)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb
index d911935005d..537908ea396 100644
--- a/spec/jobs/get_usps_proofing_results_job_spec.rb
+++ b/spec/jobs/get_usps_proofing_results_job_spec.rb
@@ -758,6 +758,16 @@
job_name: 'GetUspsProofingResultsJob',
)
end
+
+ it 'deactivates the associated profile' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
+ job.perform(Time.zone.now)
+ pending_enrollment.reload
+
+ expect(pending_enrollment.profile.active).to be false
+ expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil
+ expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled')
+ end
end
context 'when an enrollment fails and fraud is suspected' do
@@ -820,6 +830,7 @@
)
it 'deactivates the associated profile' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil
job.perform Time.zone.now
pending_enrollment.reload
@@ -889,6 +900,7 @@
end
it 'deactivates the associated profile' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil
job.perform(Time.zone.now)
@@ -1529,6 +1541,7 @@
)
it 'deactivates the associated profile' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil
job.perform(Time.zone.now)
pending_enrollment.reload
@@ -1657,6 +1670,54 @@
end
end
end
+
+ describe 'the profile has deactivation_reason set to encryption_error' do
+ context 'the enrollment passes proofing with USPS' do
+ let(:pending_enrollment) do
+ create(
+ :in_person_enrollment, :pending, :with_notification_phone_configuration
+ )
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ stub_request_passed_proofing_results
+ end
+
+ it 'overwrites the deactivation_reason' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
+ # to mimic pw reset
+ pending_enrollment.profile.update(deactivation_reason: 'encryption_error')
+ job.perform(Time.zone.now)
+ pending_enrollment.reload
+
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
+ end
+ end
+
+ context 'the enrollment fails proofing with USPS' do
+ let(:pending_enrollment) do
+ create(
+ :in_person_enrollment, :pending
+ )
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ stub_request_failed_proofing_results
+ end
+
+ it 'does not overwrite the deactivation_reason' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
+ # to mimic pw reset
+ pending_enrollment.profile.update(deactivation_reason: 'encryption_error')
+ job.perform(Time.zone.now)
+ pending_enrollment.reload
+
+ expect(pending_enrollment.profile.deactivation_reason).to eq('encryption_error')
+ end
+ end
+ end
end
describe 'Enhanced In-Person Proofing' do
@@ -1775,6 +1836,16 @@
)
end
+ it 'deactivates the associated profile' do
+ expect(pending_enrollment.profile.deactivation_reason).to be_nil
+ job.perform(Time.zone.now)
+ pending_enrollment.reload
+
+ expect(pending_enrollment.profile.active).to be false
+ expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil
+ expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled')
+ end
+
context 'when the in_person_stop_expiring_enrollments flag is true' do
before do
allow(IdentityConfig.store).to(
diff --git a/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb
new file mode 100644
index 00000000000..dad90904785
--- /dev/null
+++ b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb
@@ -0,0 +1,324 @@
+require 'rails_helper'
+
+RSpec.describe Reports::IdvLegacyConversionSupplementReport do
+ subject(:report) { Reports::IdvLegacyConversionSupplementReport.new }
+
+ before do
+ clear_agreements_data
+ ServiceProvider.delete_all
+ end
+
+ describe '#perform' do
+ it 'is empty with no data' do
+ csv = CSV.parse(report.perform(Time.zone.today), headers: true)
+ expect(csv).to be_empty
+ end
+
+ context 'with data generates reports by iaa + order number, issuer and year_month' do
+ context 'when there are converted profiles in April 2020' do
+ let(:partner_account1) { create(:partner_account) }
+ let(:iaa1_range) { DateTime.new(2020, 4, 15).utc..DateTime.new(2021, 4, 14).utc }
+ let(:gtc1) do
+ create(
+ :iaa_gtc,
+ gtc_number: 'gtc1234',
+ partner_account: partner_account1,
+ start_date: iaa1_range.begin,
+ end_date: iaa1_range.end,
+ )
+ end
+
+ let(:iaa1) { 'iaa1' }
+
+ let(:iaa1_sp) do
+ create(
+ :service_provider,
+ iaa: iaa1,
+ iaa_start_date: iaa1_range.begin,
+ iaa_end_date: iaa1_range.end,
+ )
+ end
+
+ let(:iaa_order1) do
+ build_iaa_order(order_number: 1, date_range: iaa1_range, iaa_gtc: gtc1)
+ end
+
+ let(:inside_iaa1) { iaa1_range.begin + 1.day }
+
+ let(:user1) { create(:user) }
+
+ let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) }
+
+ before do
+ iaa_order1.integrations << build_integration(
+ issuer: iaa1_sp.issuer,
+ partner_account: partner_account1,
+ )
+ iaa_order1.save
+ create(
+ :sp_upgraded_biometric_profile,
+ issuer: iaa1_sp.issuer, user_id: user1.id, upgraded_at: inside_iaa1,
+ )
+ end
+
+ it 'finds the iaa related to the upgraded profile' do
+ expect(csv.length).to eq(1)
+ aggregate_failures do
+ row = csv.find { |r| r['issuer'] == iaa1_sp.issuer }
+
+ expect(row.to_h.symbolize_keys).to eq(
+ {
+ iaa_order_number: 'gtc1234-0001',
+ iaa_start_date: '2020-04-15',
+ iaa_end_date: '2021-04-14',
+ issuer: iaa1_sp.issuer,
+ friendly_name: iaa1_sp.friendly_name,
+ year_month: '202004',
+ year_month_readable: 'April 2020',
+ user_count: '1',
+ },
+ )
+ end
+ end
+ end
+
+ context 'when there are converted profiles with two issuers in September 2020' do
+ let(:partner_account2) { create(:partner_account) }
+ let(:iaa2_range) { DateTime.new(2020, 9, 1).utc..DateTime.new(2021, 8, 30).utc }
+
+ let(:gtc2) do
+ create(
+ :iaa_gtc,
+ gtc_number: 'gtc5678',
+ partner_account: partner_account2,
+ start_date: iaa2_range.begin,
+ end_date: iaa2_range.end,
+ )
+ end
+
+ let(:iaa2) { 'iaa2' }
+
+ let(:iaa2_sp1) do
+ create(
+ :service_provider,
+ iaa: iaa2,
+ iaa_start_date: iaa2_range.begin,
+ iaa_end_date: iaa2_range.end,
+ )
+ end
+
+ let(:iaa2_sp2) do
+ create(
+ :service_provider,
+ iaa: iaa2,
+ iaa_start_date: iaa2_range.begin,
+ iaa_end_date: iaa2_range.end,
+ )
+ end
+
+ let(:iaa_order2) do
+ build_iaa_order(order_number: 2, date_range: iaa2_range, iaa_gtc: gtc2)
+ end
+
+ let(:inside_iaa2) { iaa2_range.begin + 1.day }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) }
+
+ before do
+ iaa_order2.integrations << build_integration(
+ issuer: iaa2_sp1.issuer,
+ partner_account: partner_account2,
+ )
+ iaa_order2.integrations << build_integration(
+ issuer: iaa2_sp2.issuer,
+ partner_account: partner_account2,
+ )
+
+ create(
+ :sp_upgraded_biometric_profile,
+ issuer: iaa2_sp1.issuer, user_id: user1.id, upgraded_at: inside_iaa2,
+ )
+
+ create(
+ :sp_upgraded_biometric_profile,
+ issuer: iaa2_sp2.issuer, user_id: user2.id, upgraded_at: inside_iaa2,
+ )
+ end
+
+ it 'checks values for all profile upgrades and multiple issuers for single partner' do
+ aggregate_failures do
+ row = csv.find { |r| r['issuer'] == iaa2_sp1.issuer }
+
+ expect(row.to_h.symbolize_keys).to eq(
+ {
+ iaa_order_number: 'gtc5678-0002',
+ iaa_start_date: '2020-09-01',
+ iaa_end_date: '2021-08-30',
+ issuer: iaa2_sp1.issuer,
+ friendly_name: iaa2_sp1.friendly_name,
+ year_month: '202009',
+ year_month_readable: 'September 2020',
+ user_count: '1',
+ },
+ )
+ end
+
+ aggregate_failures do
+ row = csv.find { |r| r['issuer'] == iaa2_sp2.issuer }
+
+ expect(row.to_h.symbolize_keys).to eq(
+ {
+ iaa_order_number: 'gtc5678-0002',
+ iaa_start_date: '2020-09-01',
+ iaa_end_date: '2021-08-30',
+ issuer: iaa2_sp2.issuer,
+ friendly_name: iaa2_sp2.friendly_name,
+ year_month: '202009',
+ year_month_readable: 'September 2020',
+ user_count: '1',
+ },
+ )
+ end
+ expect(csv.length).to eq(2)
+ end
+ end
+
+ context 'with multiple upgraded profiles for an issuer from September-October 2020' do
+ let(:partner_account3) { create(:partner_account) }
+
+ let(:iaa3_range) { DateTime.new(2020, 9, 1).utc..DateTime.new(2021, 8, 30).utc }
+ let(:expired_iaa_range) { DateTime.new(2019, 9, 1).utc..DateTime.new(2020, 8, 31).utc }
+
+ let(:gtc3) do
+ create(
+ :iaa_gtc,
+ gtc_number: 'gtc9101',
+ partner_account: partner_account3,
+ start_date: iaa3_range.begin,
+ end_date: iaa3_range.end,
+ )
+ end
+
+ let(:iaa3) { 'iaa3' }
+
+ let(:iaa3_sp1) do
+ create(
+ :service_provider,
+ iaa: iaa3,
+ iaa_start_date: iaa3_range.begin,
+ iaa_end_date: iaa3_range.end,
+ )
+ end
+
+ let(:iaa_order3) do
+ build_iaa_order(order_number: 3, date_range: iaa3_range, iaa_gtc: gtc3)
+ end
+
+ let(:iaa_order_expired) do
+ build_iaa_order(order_number: 2, date_range: expired_iaa_range, iaa_gtc: gtc3)
+ end
+
+ let(:integration_1) do
+ build_integration(
+ issuer: iaa3_sp1.issuer,
+ partner_account: partner_account3,
+ )
+ end
+
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) }
+
+ before do
+ iaa_order3.integrations << integration_1
+
+ create(
+ :sp_upgraded_biometric_profile,
+ issuer: iaa3_sp1.issuer, user_id: user1.id, upgraded_at: iaa3_range.begin + 1.day,
+ )
+ create(
+ :sp_upgraded_biometric_profile,
+ issuer: iaa3_sp1.issuer, user_id: user2.id, upgraded_at: iaa3_range.begin + 1.month,
+ )
+ end
+
+ it 'finds data for the same issuer with different year_months' do
+ expect(csv.length).to eq(2)
+ aggregate_failures do
+ row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer && r['year_month'] == '202009' }
+
+ expect(row['iaa_order_number']).to eq('gtc9101-0003')
+ expect(row['iaa_start_date']).to eq('2020-09-01')
+ expect(row['iaa_end_date']).to eq('2021-08-30')
+ expect(row['issuer']).to eq(iaa3_sp1.issuer)
+ expect(row['friendly_name']).to eq(iaa3_sp1.friendly_name)
+ expect(row['year_month']).to eq('202009')
+ expect(row['year_month_readable']).to eq('September 2020')
+ expect(row['user_count']).to eq('1')
+ end
+
+ aggregate_failures do
+ row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer && r['year_month'] == '202010' }
+
+ expect(row.to_h.symbolize_keys).to eq(
+ {
+ iaa_order_number: 'gtc9101-0003',
+ iaa_start_date: '2020-09-01',
+ iaa_end_date: '2021-08-30',
+ issuer: iaa3_sp1.issuer,
+ friendly_name: iaa3_sp1.friendly_name,
+ year_month: '202010',
+ year_month_readable: 'October 2020',
+ user_count: '1',
+ },
+ )
+ end
+ end
+
+ context 'when there is an expired iaa with the same issuer' do
+ before do
+ iaa_order_expired.integrations << integration_1
+ end
+
+ it 'does not include the expired iaa in the report' do
+ expect(csv.length).to eq(2)
+
+ row = csv.find { |r| r['issuer'] == iaa3_sp1.issuer }
+
+ expect(row.to_h.symbolize_keys).not_to eq(
+ {
+ iaa_order_number: 'gtc9101-0002',
+ iaa_start_date: '2019-09-01',
+ iaa_end_date: '2020-08-31',
+ issuer: iaa3_sp1.issuer,
+ friendly_name: iaa3_sp1.friendly_name,
+ year_month: '202010',
+ year_month_readable: 'October 2020',
+ user_count: '1',
+ },
+ )
+ end
+ end
+ end
+ end
+ end
+
+ def build_iaa_order(order_number:, date_range:, iaa_gtc:)
+ create(
+ :iaa_order,
+ order_number: order_number,
+ start_date: date_range.begin,
+ end_date: date_range.end,
+ iaa_gtc: iaa_gtc,
+ )
+ end
+
+ def build_integration(issuer:, partner_account:)
+ create(
+ :integration,
+ issuer: issuer,
+ partner_account: partner_account,
+ )
+ end
+end
diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb
index 7edffff6cd3..cf4b1aa3004 100644
--- a/spec/models/profile_spec.rb
+++ b/spec/models/profile_spec.rb
@@ -347,6 +347,46 @@
expect(active_profile.verified_at).to be_present
end
+ context 'when a user creates a biometric comparision profile' do
+ context 'when the user has an active profile' do
+ it 'creates a biometric upgrade record' do
+ profile.activate
+ biometric_profile = create(
+ :profile,
+ :biometric_proof,
+ user: user,
+ )
+
+ expect { biometric_profile.activate }.to(
+ change do
+ SpUpgradedBiometricProfile.count
+ end.by(1),
+ )
+ end
+ end
+
+ context 'when the user has an active biometric profile' do
+ it 'does not create a biometric conversion record' do
+ create(:profile, :active, :biometric_proof, user: user)
+
+ biometric_reproof = create(:profile, :biometric_proof, user: user)
+ expect { biometric_reproof.activate }.to_not(change { SpUpgradedBiometricProfile.count })
+ end
+ end
+
+ context 'when the user does not have an active profile' do
+ it 'does not create a biometric conversion record' do
+ profile = create(:profile, :biometric_proof, user: user)
+
+ expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count })
+ end
+ end
+ end
+
+ it 'does not create a biometric upgrade record for a non-biometric profile' do
+ expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count })
+ end
+
it 'sends a reproof completed push event' do
profile = create(:profile, :active, user: user)
expect(PushNotification::HttpPush).to receive(:deliver).
@@ -1003,12 +1043,39 @@
describe '#deactivate_due_to_in_person_verification_cancelled' do
let(:profile) { create(:profile, :in_person_verification_pending) }
- it 'updates the profile' do
- profile.deactivate_due_to_in_person_verification_cancelled
+ context 'when the profile does not have a deactivation reason' do
+ it 'updates the profile and sets the deactivation reason to "verification_cancelled"' do
+ expect(profile.deactivation_reason).to be_nil
+ profile.deactivate_due_to_in_person_verification_cancelled
+
+ expect(profile.active).to be false
+ expect(profile.deactivation_reason).to eq('verification_cancelled')
+ expect(profile.in_person_verification_pending_at).to be nil
+ end
+ end
+
+ context 'when the profile has a deactivation reason' do
+ it 'updates the profile without overwriting the deactivation reason (encryption_error)' do
+ profile.deactivation_reason = 'encryption_error'
+ expect(profile.deactivation_reason).to_not be_nil
- expect(profile.active).to be false
- expect(profile.deactivation_reason).to eq('verification_cancelled')
- expect(profile.in_person_verification_pending_at).to be nil
+ profile.deactivate_due_to_in_person_verification_cancelled
+
+ expect(profile.active).to be false
+ expect(profile.deactivation_reason).to eq('encryption_error')
+ expect(profile.in_person_verification_pending_at).to be nil
+ end
+
+ it 'updates the profile without overwriting the deactivation reason (password_reset)' do
+ profile.deactivation_reason = 'password_reset'
+ expect(profile.deactivation_reason).to_not be_nil
+
+ profile.deactivate_due_to_in_person_verification_cancelled
+
+ expect(profile.active).to be false
+ expect(profile.deactivation_reason).to eq('password_reset')
+ expect(profile.in_person_verification_pending_at).to be nil
+ end
end
end
diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb
index 14de5da2a5d..190d9edf374 100644
--- a/spec/services/proofing/aamva/request/verification_request_spec.rb
+++ b/spec/services/proofing/aamva/request/verification_request_spec.rb
@@ -113,9 +113,9 @@
stub_request(:post, config.verification_url).
to_return(body: AamvaFixtures.verification_response, status: 200)
- result = subject.send
+ response = subject.send
- expect(result.success?).to eq(true)
+ expect(response).to be_an_instance_of(Proofing::Aamva::Response::VerificationResponse)
end
it 'sends state id jurisdiction to AAMVA' do
diff --git a/spec/services/proofing/aamva/response/verification_response_spec.rb b/spec/services/proofing/aamva/response/verification_response_spec.rb
index 2bd0287eb63..d52f1165bb0 100644
--- a/spec/services/proofing/aamva/response/verification_response_spec.rb
+++ b/spec/services/proofing/aamva/response/verification_response_spec.rb
@@ -68,119 +68,17 @@
end
end
- describe '#reasons' do
- context 'when all attributes are verified' do
- it 'returns an empty array' do
- expect(subject.reasons).to eq([])
- end
-
- context 'with a namespaced XML body' do
- let(:response_body) { AamvaFixtures.verification_response_namespaced_success }
-
- it 'returns an empty array' do
- expect(subject.reasons).to eq([])
- end
- end
- end
-
- context 'when required attributes are verified' do
- let(:response_body) do
- modify_match_indicator(
- AamvaFixtures.verification_response,
- 'PersonLastNameFuzzyPrimaryMatchIndicator',
- 'false',
- )
- end
-
- it 'returns an empty array' do
- expect(subject.reasons).to eq([])
- end
- end
-
- context 'when required attributes are not verified' do
- let(:response_body) do
- body = modify_match_indicator(
- AamvaFixtures.verification_response,
- 'PersonBirthDateMatchIndicator',
- 'false',
- )
- delete_match_indicator(
- body,
- 'PersonFirstNameExactMatchIndicator',
- )
- end
-
- it 'returns an array with the reasons verification failed' do
- expect(subject.reasons).to eq(['Failed to verify dob', 'Response was missing first_name'])
- end
-
- context 'with a namespaced XML response' do
- let(:response_body) { AamvaFixtures.verification_response_namespaced_failure }
-
- it 'returns an array with the reasons verification failed' do
- expect(subject.reasons).to eq(
- [
- 'Failed to verify state_id_number',
- 'Response was missing dob',
- 'Response was missing last_name',
- 'Response was missing first_name',
- ],
- )
- end
- end
- end
- end
-
- describe '#success?' do
+ describe '#verification_results' do
context 'when all attributes are verified' do
- it { expect(subject.success?).to eq(true) }
- end
-
- context 'when required attributes are verified' do
- let(:response_body) do
- modify_match_indicator(
- AamvaFixtures.verification_response,
- 'PersonLastNameFuzzyPrimaryMatchIndicator',
- 'false',
- )
+ it 'returns a hash of values that were verified' do
+ expect(subject.verification_results).to eq(verification_results)
end
- it { expect(subject.success?).to eq(true) }
-
context 'with a namespaced XML response' do
let(:response_body) { AamvaFixtures.verification_response_namespaced_success }
- it { expect(subject.success?).to eq(true) }
- end
- end
-
- context 'when required attributes are not verified' do
- let(:response_body) do
- modify_match_indicator(
- AamvaFixtures.verification_response,
- 'PersonBirthDateMatchIndicator',
- 'false',
- )
- end
-
- it { expect(subject.success?).to eq(false) }
- end
-
- context 'when required attributes are missing' do
- let(:response_body) do
- delete_match_indicator(
- AamvaFixtures.verification_response,
- 'PersonBirthDateMatchIndicator',
- )
- end
-
- it { expect(subject.success?).to eq(false) }
- end
- end
-
- describe '#verification_results' do
- context 'when all attributes are verified' do
- it 'returns a hash of values that were verified' do
- expect(subject.verification_results).to eq(verification_results)
+ it 'returns a hash of values that were verified' do
+ expect(subject.verification_results).to eq(verification_results)
+ end
end
end
diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb
index 47f7df59d00..8f298374090 100644
--- a/spec/services/proofing/aamva/verification_client_spec.rb
+++ b/spec/services/proofing/aamva/verification_client_spec.rb
@@ -59,7 +59,22 @@
context 'when verification is successful' do
it 'returns a successful response' do
expect(response).to be_a Proofing::Aamva::Response::VerificationResponse
- expect(response.success?).to eq(true)
+ expect(response.verification_results).to eq(
+ {
+ address1: true,
+ address2: nil,
+ city: true,
+ dob: true,
+ first_name: true,
+ last_name: true,
+ state: true,
+ state_id_expiration: nil,
+ state_id_issued: nil,
+ state_id_number: true,
+ state_id_type: true,
+ zipcode: true,
+ },
+ )
end
end
@@ -75,7 +90,22 @@
it 'returns an unsuccessful response with errors' do
expect(response).to be_a Proofing::Aamva::Response::VerificationResponse
- expect(response.success?).to eq(false)
+ expect(response.verification_results).to eq(
+ {
+ address1: true,
+ address2: nil,
+ city: true,
+ dob: false,
+ first_name: true,
+ last_name: true,
+ state: true,
+ state_id_expiration: nil,
+ state_id_issued: nil,
+ state_id_number: true,
+ state_id_type: true,
+ zipcode: true,
+ },
+ )
end
end