diff --git a/app/controllers/concerns/ial2_profile_concern.rb b/app/controllers/concerns/ial2_profile_concern.rb index 44951aefbe0..679660a8d59 100644 --- a/app/controllers/concerns/ial2_profile_concern.rb +++ b/app/controllers/concerns/ial2_profile_concern.rb @@ -23,7 +23,7 @@ def cache_profile_and_handle_errors(raw_password, profile) cacher.save(raw_password, profile) rescue Encryption::EncryptionError => err if profile - profile.deactivate(:encryption_error) + profile.deactivate_due_to_encryption_error analytics.profile_encryption_invalid(error: err.message) end end diff --git a/app/models/profile.rb b/app/models/profile.rb index 2052eb33eaf..e05d3c0a33f 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -191,6 +191,20 @@ def deactivate(reason) update!(active: false, deactivation_reason: reason) end + # Update the profile's deactivation reason to "encryption_error". As a + # side-effect, when the profile has an associated pending in-person + # enrollment it will be updated to have a status of "cancelled". + def deactivate_due_to_encryption_error + update!( + active: false, + deactivation_reason: :encryption_error, + ) + + if in_person_enrollment&.pending? + in_person_enrollment.cancelled! + end + end + def fraud_deactivation_reason? fraud_review_pending? || fraud_rejection? end diff --git a/spec/controllers/concerns/ial2_profile_concern_spec.rb b/spec/controllers/concerns/ial2_profile_concern_spec.rb new file mode 100644 index 00000000000..66fdc0dbbc2 --- /dev/null +++ b/spec/controllers/concerns/ial2_profile_concern_spec.rb @@ -0,0 +1,213 @@ +require 'rails_helper' + +RSpec.describe Ial2ProfileConcern do + let(:test_controller) do + Class.new do + include Ial2ProfileConcern + + attr_accessor :current_user, :user_session, :analytics + + def initialize(current_user:, user_session:, analytics:) + @current_user = current_user + @user_session = user_session + @analytics = analytics + end + end + end + + let(:user) { create(:user) } + let(:user_session) { {} } + let(:analytics) { double(Analytics) } + let(:encryption_error) { Encryption::EncryptionError.new } + let(:cacher) { double(Pii::Cacher) } + let(:password) { 'PraiseTheSun!' } + + describe '#cache_profiles' do + subject { test_controller.new(current_user: user, user_session:, analytics:) } + + context 'when the user has a pending profile' do + let!(:profile) { create(:profile, :in_person_verification_pending, user:) } + + context 'when the profile can be saved to cache' do + before do + allow(cacher).to receive(:save) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + subject.cache_profiles(password) + end + + it 'stores the decrypted profile in cache' do + expect(cacher).to have_received(:save).with(password, profile) + end + end + + context 'when the profile can not be saved to cache' do + before do + allow(cacher).to receive(:save).and_raise(encryption_error) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + allow(analytics).to receive(:profile_encryption_invalid) + subject.cache_profiles(password) + end + + it 'deactivates the profile with reason encryption_error' do + expect(profile.reload).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'logs the profile_encryption_invalid analytic' do + expect(analytics).to have_received(:profile_encryption_invalid).with( + error: encryption_error.message, + ) + end + end + end + + context 'when the user has an active profile' do + let!(:profile) { create(:profile, :active, user:) } + + context 'when the profile can be saved to cache' do + before do + allow(cacher).to receive(:save) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + subject.cache_profiles(password) + end + + it 'stores the decrypted profile in cache' do + expect(cacher).to have_received(:save).with(password, profile) + end + end + + context 'when the profile can not be saved to cache' do + before do + allow(cacher).to receive(:save).and_raise(encryption_error) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + allow(analytics).to receive(:profile_encryption_invalid) + subject.cache_profiles(password) + end + + it 'deactivates the profile with reason encryption_error' do + expect(profile.reload).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'logs the profile_encryption_invalid analytic' do + expect(analytics).to have_received(:profile_encryption_invalid).with( + error: encryption_error.message, + ) + end + end + end + + context 'when the user has both an active profile and pending profile' do + let(:pending_profile) { create(:profile, :in_person_verification_pending, user:) } + let(:active_profile) { create(:profile, :active, user:) } + + context 'when the active profile was activated before the pending profile was created' do + before do + pending_profile.update!(created_at: Time.zone.now) + active_profile.update!(activated_at: 1.day.ago) + end + + context 'when the profiles can be saved to cache' do + before do + allow(cacher).to receive(:save) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + subject.cache_profiles(password) + end + + it 'stores the decrypted pending profile in cache' do + expect(cacher).to have_received(:save).with(password, pending_profile) + end + + it 'stores the decrypted active profile in cache' do + expect(cacher).to have_received(:save).with(password, active_profile) + end + end + + context 'when the profile can not be saved to cache' do + before do + allow(cacher).to receive(:save).and_raise(encryption_error) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + allow(analytics).to receive(:profile_encryption_invalid) + subject.cache_profiles(password) + end + + it 'deactivates the pending profile with reason encryption_error' do + expect(pending_profile.reload).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'deactivates the active profile with reason encryption_error' do + expect(active_profile.reload).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'logs the profile_encryption_invalid analytic' do + expect(analytics).to have_received(:profile_encryption_invalid).with( + error: encryption_error.message, + ).twice + end + end + end + + context 'when the active profile was activated after the pending profile was created' do + before do + pending_profile.update!(created_at: 1.day.ago) + active_profile.update!(activated_at: Time.zone.now) + end + + context 'when the profiles can be saved to cache' do + before do + allow(cacher).to receive(:save) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + subject.cache_profiles(password) + end + + it 'does not store the decrypted pending profile in cache' do + expect(cacher).not_to have_received(:save).with(password, pending_profile) + end + + it 'stores the decrypted active profile in cache' do + expect(cacher).to have_received(:save).with(password, active_profile) + end + end + + context 'when the profile can not be saved to cache' do + before do + allow(cacher).to receive(:save).and_raise(encryption_error) + allow(Pii::Cacher).to receive(:new).and_return(cacher) + allow(analytics).to receive(:profile_encryption_invalid) + subject.cache_profiles(password) + end + + it 'does not deactivate the pending profile with reason encryption_error' do + expect(pending_profile.reload).to have_attributes( + active: false, + deactivation_reason: nil, + ) + end + + it 'deactivates the active profile with reason encryption_error' do + expect(active_profile.reload).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'logs the profile_encryption_invalid analytic' do + expect(analytics).to have_received(:profile_encryption_invalid).with( + error: encryption_error.message, + ).once + end + end + end + end + end +end diff --git a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb index 08be64fae88..f28bc7fe37c 100644 --- a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb +++ b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb @@ -51,9 +51,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "pending" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'pending', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -78,7 +78,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), ) # When the user logs in @@ -94,7 +94,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), ) end @@ -130,9 +130,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "pending" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'pending', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -157,7 +157,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), ) # When the user logs in @@ -173,7 +173,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), ) end end @@ -537,9 +537,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "pending" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'pending', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" and # pending in person verification and fraud review @@ -568,7 +568,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), fraud_pending_reason: 'threatmetrix_review', fraud_review_pending_at: be_kind_of(Time), ) @@ -587,7 +587,7 @@ expect(@user.in_person_enrollments.first.profile).to have_attributes( active: false, deactivation_reason: 'encryption_error', - in_person_verification_pending_at: nil, + in_person_verification_pending_at: be_kind_of(Time), fraud_pending_reason: 'threatmetrix_review', fraud_review_pending_at: be_kind_of(Time), ) diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 57f9631c173..336c6eee4d4 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -609,6 +609,68 @@ end end + describe '#deactivate_due_to_encryption_error' do + context 'when the profile has a "pending" in_person_enrollment' do + subject { create(:profile, :in_person_verification_pending, user: user) } + let!(:enrollment) do + create(:in_person_enrollment, user: user, profile: subject, status: :pending) + end + + before do + subject.deactivate_due_to_encryption_error + end + + it 'deactivates with reason encryption_error' do + expect(subject).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + in_person_verification_pending_at: be_kind_of(Time), + ) + end + + it 'cancels the associated pending in_person_enrollment' do + expect(subject.in_person_enrollment.status).to eq('cancelled') + end + end + + context 'when the profile has a "passed" in_person_enrollment' do + subject { create(:profile, :active, user: user) } + let!(:enrollment) do + create(:in_person_enrollment, user: user, profile: subject, status: :passed) + end + + before do + subject.deactivate_due_to_encryption_error + end + + it 'deactivates with reason encryption_error' do + expect(subject).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + + it 'does not cancel the associated pending in_person_enrollment' do + expect(subject.in_person_enrollment.status).to eq('passed') + end + end + + context 'when the profile has no in_person_enrollment' do + subject { create(:profile, :active, user: user) } + + before do + subject.deactivate_due_to_encryption_error + end + + it 'deactivates with reason encryption_error' do + expect(subject).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + ) + end + end + end + describe '#remove_gpo_deactivation_reason' do it 'removes the gpo_verification_pending_at deactivation reason' do profile = create(:profile, :verify_by_mail_pending)