diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index 76f0654177b..d49b3fe4dd7 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -55,6 +55,8 @@ def create result = @gpo_verify_form.submit(resolved_authn_context_result.enhanced_ipp?) analytics.idv_verify_by_mail_enter_code_submitted(**result) + send_please_call_email_if_necessary(result:) + if !result.success? if rate_limiter.limited? redirect_to idv_enter_code_rate_limited_url @@ -120,6 +122,19 @@ def rate_limiter ) end + # @param [FormResponse] result GpoVerifyForm result + def send_please_call_email_if_necessary(result:) + return if !result.success? + + return if result.extra[:pending_in_person_enrollment] + + return if !result.extra[:fraud_check_failed] + + return if !FeatureManagement.proofing_device_profiling_decisioning_enabled? + + current_user.send_email_to_all_addresses(:idv_please_call) + end + def build_gpo_verify_form GpoVerifyForm.new( user: current_user, diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index af6da96312f..dc5bdfaa327 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -143,6 +143,10 @@ def init_profile log_letter_enqueued_analytics(resend: false) end + if profile.fraud_review_pending? && !profile.in_person_verification_pending? + current_user.send_email_to_all_addresses(:idv_please_call) + end + if profile.active? create_user_event(:account_verified) UserAlerts::AlertUserAboutAccountVerified.call( diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 78ad7a08c5b..ebb16537316 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -610,7 +610,7 @@ def send_failed_fraud_email(enrollment:, visited_location_name:) def send_please_call_email(enrollment:, visited_location_name:) enrollment.user.confirmed_email_addresses.each do |email_address| # rubocop:disable IdentityIdp/MailLaterLinter - UserMailer.with(user: enrollment.user, email_address: email_address).in_person_please_call( + UserMailer.with(user: enrollment.user, email_address: email_address).idv_please_call( enrollment: enrollment, visited_location_name: visited_location_name, ).deliver_later(**notification_delivery_params(enrollment)) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 46e480dab2a..eb623771eed 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -249,6 +249,9 @@ def account_verified(profile:) end def idv_please_call(**) + attachments.inline['phone_icon.png'] = + Rails.root.join('app/assets/images/email/phone_icon.png').read + with_user_locale(user) do @hide_title = true diff --git a/app/views/user_mailer/idv_please_call.html.erb b/app/views/user_mailer/idv_please_call.html.erb index 54f4a89ba9f..989409d1602 100644 --- a/app/views/user_mailer/idv_please_call.html.erb +++ b/app/views/user_mailer/idv_please_call.html.erb @@ -1,5 +1,5 @@ <%= image_tag( - asset_url('email/phone_icon.png'), + attachments['phone_icon.png'].url, alt: t('image_description.phone_icon'), width: 88, height: 88, diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb index 5e0a8581413..e9f5ef0f835 100644 --- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb +++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb @@ -285,6 +285,14 @@ expect(event_count).to eq 1 expect(response).to redirect_to(idv_personal_key_url) end + + it 'does not send the "Please Call" email' do + action + expect_email_not_delivered( + to: user.confirmed_email_addresses.first.email, + subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), + ) + end end end @@ -323,6 +331,14 @@ expect(UserAlerts::AlertUserAboutAccountVerified).not_to have_received(:call) end + + it 'sends the "Please Call" email' do + action + expect_delivered_email( + to: user.confirmed_email_addresses.first.email, + subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), + ) + end end context 'with threatmetrix status of "review"' do diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index fcb3b8b8e70..a96a19998d4 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -13,6 +13,8 @@ end let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE } let(:use_gpo) { false } + let(:threatmetrix_enabled) { true } + let(:threatmetrix_result) { 'pass' } let(:idv_session) do subject.idv_session end @@ -28,6 +30,7 @@ subject.idv_session.pii_from_doc = Pii::StateId.new(**Idp::Constants::MOCK_IDV_APPLICANT) subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:ssn] subject.idv_session.threatmetrix_session_id = 'random-session-id' + subject.idv_session.threatmetrix_review_status = threatmetrix_result subject.idv_session.resolution_successful = true subject.idv_session.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE subject.idv_session.resolution_successful = true @@ -43,6 +46,9 @@ end subject.idv_session.applicant = applicant.with_indifferent_access + + allow(IdentityConfig.store).to receive(:proofing_device_profiling) + .and_return(threatmetrix_enabled ? :enabled : :disabled) end describe '#step_info' do @@ -404,6 +410,41 @@ def show expect(events_count).to eq 1 end + context 'user was flagged by ThreatMetrix' do + let(:threatmetrix_result) { 'reject' } + + it 'sends the idv_please_call email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_delivered_email( + to: user.confirmed_email_addresses.first.email, + subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), + ) + end + + it 'does not send the account_verified email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_email_not_delivered( + subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), + ) + end + + context 'but ThreatMetrix disabled' do + let(:threatmetrix_enabled) { false } + it 'does not send the idv_please_call email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_email_not_delivered( + subject: t('user_mailer.idv_please_call.subject'), + ) + end + it 'sends the account_verified email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_delivered_email( + subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), + ) + end + end + end + context 'with in person profile' do let!(:enrollment) do create(:in_person_enrollment, :establishing, user: user, profile: nil) @@ -472,6 +513,17 @@ def show ) end + context 'user was flagged by ThreatMetrix' do + let(:threatmetrix_result) { 'reject' } + + it 'does not send the idv_please_call email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_email_not_delivered( + subject: t('user_mailer.idv_please_call.subject'), + ) + end + end + context 'when there is a 4xx error' do before do stub_request_enroll_bad_request_response @@ -965,6 +1017,27 @@ def show expect(user.reload.gpo_verification_pending_profile).to be_nil end end + + context 'user was flagged by ThreatMetrix' do + let(:threatmetrix_review_status) { 'reject' } + + it 'does not send the idv_please_call email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_email_not_delivered( + subject: t('user_mailer.idv_please_call.subject'), + ) + end + + context 'but ThreatMetrix disabled' do + let(:threatmetrix_enabled) { false } + it 'does not send the idv_please_call email' do + put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } + expect_email_not_delivered( + subject: t('user_mailer.idv_please_call.subject'), + ) + end + end + end end context 'user is going through enhanced ipp' do diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 99ff7bfaa0a..5ea9d8b9131 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -1490,7 +1490,7 @@ allow(analytics).to receive( :idv_in_person_usps_proofing_results_job_please_call_email_initiated, ) - allow(user_mailer).to receive(:in_person_please_call).and_return(mail_deliverer) + allow(user_mailer).to receive(:idv_please_call).and_return(mail_deliverer) subject.perform(current_time) end @@ -1545,7 +1545,7 @@ end it 'sends the please call email' do - expect(user_mailer).to have_received(:in_person_please_call).with( + expect(user_mailer).to have_received(:idv_please_call).with( enrollment: enrollment, visited_location_name: visited_location_name, ) diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 5ac90eb4ebf..c33ece27391 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -814,6 +814,13 @@ def expect_email_body_to_have_help_and_contact_links mail.deliver_later end + + it 'attaches the icon inline' do + icon_part = mail.attachments['phone_icon.png'] + expect(icon_part).not_to be(nil) + expect(icon_part.inline?).to eql(true) + expect(icon_part.url).to start_with('cid:') + end end context 'in person emails' do diff --git a/spec/services/user_alerts/alert_user_about_account_verified_spec.rb b/spec/services/user_alerts/alert_user_about_account_verified_spec.rb index 47dd8f362fa..c956abd7ba1 100644 --- a/spec/services/user_alerts/alert_user_about_account_verified_spec.rb +++ b/spec/services/user_alerts/alert_user_about_account_verified_spec.rb @@ -38,7 +38,9 @@ expect_delivered_email( to: [user.confirmed_email_addresses.first.email], subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), - body: ['', 'localhost:3000'], + body: [ + 'http://www.example.com/redirect/return_to_sp/account_verified_cta', + ], ) end end @@ -50,7 +52,9 @@ described_class.call(profile: profile) email_body = last_email.text_part.decoded.squish - expect(email_body).to_not include('
') + expect(email_body).to_not include( + 'http://www.example.com/redirect/return_to_sp/account_verified_cta', + ) end end @@ -69,7 +73,7 @@ expect_delivered_email( to: [user.confirmed_email_addresses.first.email], subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), - body: ['
', 'http://example.com'], + body: ['http://example.com'], ) end end diff --git a/spec/support/mailer_helper.rb b/spec/support/mailer_helper.rb index eb264b0adca..e28e65b22cf 100644 --- a/spec/support/mailer_helper.rb +++ b/spec/support/mailer_helper.rb @@ -15,26 +15,106 @@ def strip_tags(str) ActionController::Base.helpers.strip_tags(str) end + # @param [String,String[],nil] to The email address(es) the message must've been sent to. + # @param [String,nil] subject The subject the email must've had + # @param [String[],nil] Array of substrings that must appear in body. def expect_delivered_email(to: nil, subject: nil, body: nil) - email = ActionMailer::Base.deliveries.find do |sent_mail| - next unless to.present? && sent_mail.to == to - next unless subject.present? && sent_mail.subject == subject - if body.present? - delivered_body = sent_mail.text_part.decoded.squish - body.to_a.each do |expected_body| - next unless delivered_body.include?(expected_body) - end - end - true - end + email = find_sent_email(to:, subject:, body:) error_message = <<~ERROR Unable to find email matching args: to: #{to} subject: #{subject} body: #{body} - Sent mails: #{ActionMailer::Base.deliveries} + Sent mails: + #{summarize_all_deliveries(to:, subject:, body:).indent(2)} ERROR + expect(email).to_not be(nil), error_message end + + # @param [String,nil] to If provided, the email address the message must've been sent to + # @param [String,nil] subject If provided, the subject the email must've had + # @param [String[],nil] Array of substrings that must appear in body. + def expect_email_not_delivered(to: nil, subject: nil, body: nil) + email = find_sent_email(to:, subject:, body:) + + error_message = <<~ERROR + Found an email matching the below (but shouldn't have): + to: #{to} + subject: #{subject} + body: #{body} + Sent mails: + #{summarize_all_deliveries(to:, subject:, body:).indent(2)} + ERROR + + expect(email).to be(nil), error_message + end + + private + + def body_matches(email:, body:) + return true if body.nil? + + delivered_body = email.text_part.decoded.squish + + Array.wrap(body).all? do |expected_substring| + delivered_body.include?(expected_substring) + end + end + + def to_matches(email:, to:) + return true if to.nil? + + to = Array.wrap(to).to_set + + (email.to.to_set - to).empty? + end + + def find_sent_email( + to:, + subject:, + body: + ) + ActionMailer::Base.deliveries.find do |email| + to_ok = to_matches(email:, to:) + subject_ok = subject.nil? || email.subject == subject + body_ok = body_matches(email:, body:) + + to_ok && subject_ok && body_ok + end + end + + def summarize_delivery( + email:, + to:, + subject:, + body: + ) + body_text = email.text_part.decoded.squish + + body_summary = body.presence && Array.wrap(body).map do |substring| + found = body_text.include?(substring) + "- #{substring.inspect} (#{found ? 'found' : 'not found'})" + end.join("\n") + + to_ok = to_matches(email:, to:) + subject_ok = subject.nil? || subject == email.subject + + [ + "To: #{email.to}#{to_ok ? '' : ' (did not match)'}", + "Subject: #{email.subject}#{subject_ok ? '' : ' (did not match)'}", + body.presence && "Body:\n#{body_summary.indent(2)}", + ].compact.join("\n") + end + + def summarize_all_deliveries(query) + ActionMailer::Base.deliveries.map do |email| + summary = summarize_delivery(email:, **query) + [ + "- #{summary.lines.first.chomp}", + *summary.lines.drop(1).map { |l| l.chomp.indent(2) }, + ].join("\n") + end.join("\n") + end end diff --git a/spec/support/mailer_helper_spec.rb b/spec/support/mailer_helper_spec.rb new file mode 100644 index 00000000000..dbec5de7d64 --- /dev/null +++ b/spec/support/mailer_helper_spec.rb @@ -0,0 +1,383 @@ +require 'rails_helper' + +RSpec.describe 'mailer_helper' do + def mail_double(to:, subject:, body:) + instance_double( + Mail::Message, + to:, + subject:, + text_part: instance_double( + Mail::Part, + decoded: body, + ), + ) + end + + let(:all_deliveries) do + [ + mail_double( + to: ['user@example.com'], + subject: 'Test subject 1', + body: 'Hello world!', + ), + ] + end + + before do + allow(ActionMailer::Base).to receive(:deliveries).and_return(all_deliveries) + end + + describe '#expect_delivered_email' do + context 'when searching by to' do + context 'and found' do + it 'does not raise' do + expect do + expect_delivered_email(to: 'user@example.com') + end.not_to raise_error + end + end + context 'and not found' do + it 'raises an appropriate error' do + expect do + expect_delivered_email(to: 'otheruser@example.com') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Unable to find email matching args: + to: otheruser@example.com + subject: + body: + Sent mails: + - To: [\"user@example.com\"] (did not match) + Subject: Test subject 1 + END + ) + end, + ) + end + end + end + + context 'when searching by subject' do + context 'and found' do + it 'does not raise' do + expect do + expect_delivered_email(subject: 'Test subject 1') + end.not_to raise_error + end + context 'and not found' do + it 'raises an appropriate error' do + expect do + expect_delivered_email(subject: 'Another unrelated subject') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Unable to find email matching args: + to: + subject: Another unrelated subject + body: + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 (did not match) + END + ) + end, + ) + end + end + end + end + + context 'when searching by body' do + context 'with string' do + context 'when found' do + it 'does not raise' do + expect do + expect_delivered_email( + body: 'Hello', + ) + end.not_to raise_error + end + end + context 'and not found' do + it 'raises an appropriate error' do + expect do + expect_delivered_email(body: 'Hellow') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Unable to find email matching args: + to: + subject: + body: Hellow + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + Body: + - "Hellow" (not found) + END + ) + end, + ) + end + end + end + context 'with array' do + context 'when found' do + it 'does not raise' do + expect do + expect_delivered_email( + body: ['Hello', 'world'], + ) + end.not_to raise_error + end + end + context 'and not found' do + it 'raises an appropriate error' do + expect do + expect_delivered_email(body: ['Hellow', 'world']) + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Unable to find email matching args: + to: + subject: + body: ["Hellow", "world"] + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + Body: + - "Hellow" (not found) + - "world" (found) + END + ) + end, + ) + end + end + end + end + + context 'when searching by to + subject + body' do + context 'and found' do + it 'does not raise' do + expect do + expect_delivered_email( + to: 'user@example.com', + subject: 'Test subject 1', + body: 'Hello', + ) + end.not_to raise_error + end + context 'and to does not match any' do + it 'raises an appropriate error' do + expect do + expect_delivered_email( + to: 'otheruser@example.com', + subject: 'Unrelated subject', + body: 'Hellow', + ) + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Unable to find email matching args: + to: otheruser@example.com + subject: Unrelated subject + body: Hellow + Sent mails: + - To: [\"user@example.com\"] (did not match) + Subject: Test subject 1 (did not match) + Body: + - "Hellow" (not found) + END + ) + end, + ) + end + end + end + end + end + + describe '#expect_email_not_delivered' do + context 'when searching by to' do + context 'and not found' do + it 'does not raise' do + expect do + expect_email_not_delivered(to: 'otheruser@example.com') + end.not_to raise_error + end + end + context 'and found' do + it 'raises an appropriate error' do + expect do + expect_email_not_delivered(to: 'user@example.com') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Found an email matching the below (but shouldn't have): + to: user@example.com + subject: + body: + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + END + ) + end, + ) + end + end + end + + context 'when searching by subject' do + context 'and not found' do + it 'does not raise' do + expect do + expect_email_not_delivered(subject: 'Another unrelated subject') + end.not_to raise_error + end + context 'and found' do + it 'raises an appropriate error' do + expect do + expect_email_not_delivered(subject: 'Test subject 1') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Found an email matching the below (but shouldn't have): + to: + subject: Test subject 1 + body: + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + END + ) + end, + ) + end + end + end + end + + context 'when searching by body' do + context 'with string' do + context 'when not found' do + it 'does not raise' do + expect do + expect_email_not_delivered( + body: 'Hellow', + ) + end.not_to raise_error + end + end + context 'when found' do + it 'raises an appropriate error' do + expect do + expect_email_not_delivered(body: 'Hello') + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Found an email matching the below (but shouldn't have): + to: + subject: + body: Hello + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + Body: + - "Hello" (found) + END + ) + end, + ) + end + end + end + context 'with array' do + context 'when not found' do + it 'does not raise' do + expect do + expect_email_not_delivered( + body: ['Hellow', 'world'], + ) + end.not_to raise_error + end + end + context 'and found' do + it 'raises an appropriate error' do + expect do + expect_email_not_delivered(body: ['Hello', 'world']) + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Found an email matching the below (but shouldn't have): + to: + subject: + body: ["Hello", "world"] + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + Body: + - "Hello" (found) + - "world" (found) + END + ) + end, + ) + end + end + end + end + + context 'when searching by to + subject + body' do + context 'and not found' do + it 'does not raise' do + expect do + expect_email_not_delivered( + to: 'otheruser@example.com', + subject: 'Unrelated subject', + body: 'Hellow', + ) + end.not_to raise_error + end + context 'and it matches' do + it 'raises an appropriate error' do + expect do + expect_email_not_delivered( + to: 'user@example.com', + subject: 'Test subject 1', + body: 'Hello', + ) + end.to raise_error( + satisfy do |err| + expect(err.message).to eql( + <<~END, + Found an email matching the below (but shouldn't have): + to: user@example.com + subject: Test subject 1 + body: Hello + Sent mails: + - To: [\"user@example.com\"] + Subject: Test subject 1 + Body: + - "Hello" (found) + END + ) + end, + ) + end + end + end + end + end +end