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 32678fad843..70d27f774c9 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -248,6 +248,21 @@ def account_verified(profile:) end 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 + + mail( + to: email_address.email, + subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), + template_name: 'idv_please_call', + ) + end + end + def in_person_completion_survey with_user_locale(user) do @header = t('user_mailer.in_person_completion_survey.header') @@ -373,21 +388,6 @@ def in_person_failed_fraud(enrollment:, visited_location_name: nil) end end - def in_person_please_call(enrollment:, visited_location_name: nil) - with_user_locale(user) do - @presenter = Idv::InPerson::VerificationResultsEmailPresenter.new( - enrollment: enrollment, - url_options: url_options, - visited_location_name: visited_location_name, - ) - @hide_title = true - mail( - to: email_address.email, - subject: t('user_mailer.in_person_please_call.subject', app_name: APP_NAME), - ) - end - end - def account_rejected with_user_locale(user) do mail( diff --git a/app/views/user_mailer/in_person_please_call.html.erb b/app/views/user_mailer/idv_please_call.html.erb similarity index 62% rename from app/views/user_mailer/in_person_please_call.html.erb rename to app/views/user_mailer/idv_please_call.html.erb index f8cef7a4ae8..989409d1602 100644 --- a/app/views/user_mailer/in_person_please_call.html.erb +++ b/app/views/user_mailer/idv_please_call.html.erb @@ -1,19 +1,19 @@ <%= image_tag( - asset_url('email/phone_icon.png'), + attachments['phone_icon.png'].url, alt: t('image_description.phone_icon'), width: 88, height: 88, ) %> -

<%= t('user_mailer.in_person_please_call.header') %>

+

<%= t('user_mailer.idv_please_call.header') %>

<%= t( - 'user_mailer.in_person_please_call.body.intro_html', + 'user_mailer.idv_please_call.body.intro_html', date: I18n.l(14.days.from_now, format: I18n.t('time.formats.full_date')), ) %>

<%= t( - 'user_mailer.in_person_please_call.body.contact_message_html', + 'user_mailer.idv_please_call.body.contact_message_html', contact_number: IdentityConfig.store.idv_contact_phone_number, support_code: IdentityConfig.store.lexisnexis_threatmetrix_support_code, ) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 89b9f965ec0..a999cd2a1cd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1875,6 +1875,10 @@ user_mailer.email_deleted.header: An email address was deleted from your %{app_n user_mailer.email_deleted.help_html: If you did not want to delete this email address, please visit the %{app_name_html} %{help_link_html} or %{contact_link_html}. user_mailer.email_deleted.subject: Email address deleted user_mailer.help_link_text: Help Center +user_mailer.idv_please_call.body.contact_message_html: Call %{contact_number} and provide them with the error code %{support_code}. +user_mailer.idv_please_call.body.intro_html: Call our contact center by %{date} to continue verifying your identity. +user_mailer.idv_please_call.header: Please give us a call +user_mailer.idv_please_call.subject: Call %{app_name} to continue with your identity verification user_mailer.in_person_completion_survey.body.cta.callout: Click the button below to get started. user_mailer.in_person_completion_survey.body.cta.label: Take our survey user_mailer.in_person_completion_survey.body.greeting: Hello, @@ -1900,10 +1904,6 @@ user_mailer.in_person_failed.intro: Your identity could not be verified at the % user_mailer.in_person_failed.subject: Your identity could not be verified in person user_mailer.in_person_failed.verifying_identity: 'When verifying your identity:' user_mailer.in_person_failed.verifying_step_not_expired: Your state‑issued ID or driver’s license must not be expired. We do not currently accept any other forms of identification, such as passports and military IDs. -user_mailer.in_person_please_call.body.contact_message_html: Call %{contact_number} and provide them with the error code %{support_code}. -user_mailer.in_person_please_call.body.intro_html: Call our contact center by %{date} to continue verifying your identity. -user_mailer.in_person_please_call.header: Please give us a call -user_mailer.in_person_please_call.subject: Call %{app_name} to continue with your identity verification user_mailer.in_person_ready_to_verify_reminder.greeting: Hello, user_mailer.in_person_ready_to_verify_reminder.heading.one: You have %{count} day left to verify your identity in person user_mailer.in_person_ready_to_verify_reminder.heading.other: You have %{count} days left to verify your identity in person diff --git a/config/locales/es.yml b/config/locales/es.yml index e790124af69..57b1e9bf60f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1887,6 +1887,10 @@ user_mailer.email_deleted.header: Se eliminó una dirección de correo electrón user_mailer.email_deleted.help_html: Si no deseaba eliminar esta dirección de correo electrónico, visite %{help_link_html} de %{app_name_html} o %{contact_link_html}. user_mailer.email_deleted.subject: Dirección de correo electrónico eliminada user_mailer.help_link_text: Centro de ayuda +user_mailer.idv_please_call.body.contact_message_html: Llame al %{contact_number} y proporcione el código de error %{support_code}. +user_mailer.idv_please_call.body.intro_html: Llame a nuestro centro de contacto antes del %{date} para seguir verificando su identidad. +user_mailer.idv_please_call.header: Llámenos +user_mailer.idv_please_call.subject: Llame a %{app_name} para continuar con la verificación de identidad user_mailer.in_person_completion_survey.body.cta.callout: Haga clic en el botón siguiente para empezar. user_mailer.in_person_completion_survey.body.cta.label: Responda a nuestra encuesta user_mailer.in_person_completion_survey.body.greeting: 'Hola:' @@ -1912,10 +1916,6 @@ user_mailer.in_person_failed.intro: No se pudo verificar su identidad en la ofic user_mailer.in_person_failed.subject: No se pudo verificar su identidad en persona user_mailer.in_person_failed.verifying_identity: 'Cuando verifique su identidad:' user_mailer.in_person_failed.verifying_step_not_expired: Su licencia de conducir o identificación emitida por el estado debe estar vigente. Actualmente no aceptamos otras formas de identificación, como pasaportes o identificaciones militares. -user_mailer.in_person_please_call.body.contact_message_html: Llame al %{contact_number} y proporcione el código de error %{support_code}. -user_mailer.in_person_please_call.body.intro_html: Llame a nuestro centro de contacto antes del %{date} para seguir verificando su identidad. -user_mailer.in_person_please_call.header: Llámenos -user_mailer.in_person_please_call.subject: Llame a %{app_name} para continuar con la verificación de identidad user_mailer.in_person_ready_to_verify_reminder.greeting: 'Hola:' user_mailer.in_person_ready_to_verify_reminder.heading.one: Le queda %{count} día para verificar su identidad en persona user_mailer.in_person_ready_to_verify_reminder.heading.other: Le quedan %{count} días para verificar su identidad en persona diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6775afb05c9..5ea53a3c147 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1875,6 +1875,10 @@ user_mailer.email_deleted.header: Une adresse e-mail a été supprimée de votre user_mailer.email_deleted.help_html: Si vous ne souhaitez pas supprimer cette adresse e-mail, veuillez visiter le %{help_link_html} de %{app_name_html} ou %{contact_link_html}. user_mailer.email_deleted.subject: Adresse e-mail supprimée user_mailer.help_link_text: Centre d’aide +user_mailer.idv_please_call.body.contact_message_html: Appelez le %{contact_number} et indiquez le code d’erreur %{support_code}. +user_mailer.idv_please_call.body.intro_html: Appelez notre centre de contact avant le %{date} pour continuer à vérifier votre identité. +user_mailer.idv_please_call.header: S’il vous plaît, appelez-nous +user_mailer.idv_please_call.subject: Appeler %{app_name} afin de poursuivre la vérification de votre identité user_mailer.in_person_completion_survey.body.cta.callout: Cliquez sur le bouton ci-dessous pour commencer. user_mailer.in_person_completion_survey.body.cta.label: Répondez à notre enquête user_mailer.in_person_completion_survey.body.greeting: Bonjour, @@ -1900,10 +1904,6 @@ user_mailer.in_person_failed.intro: Votre identité n’a pas pu être vérifié user_mailer.in_person_failed.subject: Votre identité n’a pas pu être vérifiée en personne user_mailer.in_person_failed.verifying_identity: 'Lors de la vérification de votre identité :' user_mailer.in_person_failed.verifying_step_not_expired: Votre carte d’identité délivrée par l’État ou votre permis de conduire ne doit pas être périmé. Nous n’acceptons actuellement aucune autre pièce d’identité, comme les passeports et les cartes d’identité militaires. -user_mailer.in_person_please_call.body.contact_message_html: Appelez le %{contact_number} et indiquez le code d’erreur %{support_code}. -user_mailer.in_person_please_call.body.intro_html: Appelez notre centre de contact avant le %{date} pour continuer à vérifier votre identité. -user_mailer.in_person_please_call.header: S’il vous plaît, appelez-nous -user_mailer.in_person_please_call.subject: Appeler %{app_name} afin de poursuivre la vérification de votre identité user_mailer.in_person_ready_to_verify_reminder.greeting: Bonjour, user_mailer.in_person_ready_to_verify_reminder.heading.one: Il vous reste %{count} jour pour vérifier votre identité en personne user_mailer.in_person_ready_to_verify_reminder.heading.other: Il vous reste %{count} jours pour vérifier votre identité en personne diff --git a/config/locales/zh.yml b/config/locales/zh.yml index aaebc735fad..2797d52219e 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1888,6 +1888,10 @@ user_mailer.email_deleted.header: 一个电邮地址被从你的 %{app_name} 用 user_mailer.email_deleted.help_html: 如果你没有想删除这一电邮地址,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。 user_mailer.email_deleted.subject: 电邮地址已删除 user_mailer.help_link_text: 帮助中心 +user_mailer.idv_please_call.body.contact_message_html: 打电话给 %{contact_number} 并向他们提供错误 代码 %{support_code}。 +user_mailer.idv_please_call.body.intro_html: 请在 %{date} 之前给我们的联系中心打电话,以继续验证你的身份。 +user_mailer.idv_please_call.header: 请给我们打个电话 +user_mailer.idv_please_call.subject: 致电 %{app_name} 继续进行身份验证 user_mailer.in_person_completion_survey.body.cta.callout: 点击下面的按钮来开始 user_mailer.in_person_completion_survey.body.cta.label: 填写我们的意见调查 user_mailer.in_person_completion_survey.body.greeting: 你好, @@ -1913,10 +1917,6 @@ user_mailer.in_person_failed.intro: 你的身份于 %{date}在 %{location} 邮 user_mailer.in_person_failed.subject: 你的身份未能亲身被验证。 user_mailer.in_person_failed.verifying_identity: '验证你的身份时:' user_mailer.in_person_failed.verifying_step_not_expired: 你的州政府颁发的身份证件或驾照绝对没有过期。我们目前不接受任何其他形式的身份证件,比如护照和军队身份证件。 -user_mailer.in_person_please_call.body.contact_message_html: 打电话给 %{contact_number} 并向他们提供错误 代码 %{support_code}。 -user_mailer.in_person_please_call.body.intro_html: 请在 %{date} 之前给我们的联系中心打电话,以继续验证你的身份。 -user_mailer.in_person_please_call.header: 请给我们打个电话 -user_mailer.in_person_please_call.subject: 致电 %{app_name} 继续进行身份验证 user_mailer.in_person_ready_to_verify_reminder.greeting: 你好, user_mailer.in_person_ready_to_verify_reminder.heading.one: 你距离亲身验证身份截止日期还有 %{count} 天 user_mailer.in_person_ready_to_verify_reminder.heading.other: 你距离亲身验证身份截止日期还有 %{count} 天 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/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 297b4c5a7f9..66edf8f05ee 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -243,11 +243,8 @@ def in_person_failed_fraud ) end - def in_person_please_call - UserMailer.with(user: user, email_address: email_address_record).in_person_please_call( - enrollment: in_person_enrollment_id_ipp, - visited_location_name: in_person_visited_location_name, - ) + def idv_please_call + UserMailer.with(user: user, email_address: email_address_record).idv_please_call end def account_rejected diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 8e273ac41ef..3a2c9142d71 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -799,6 +799,30 @@ def expect_email_body_to_have_help_and_contact_links end end + describe '#idv_please_call' do + let(:mail) do + UserMailer.with(user: user, email_address: email_address).idv_please_call + end + + it_behaves_like 'a system email' + it_behaves_like 'an email that respects user email locale preference' + + it 'renders the idv_please_call template' do + expect_any_instance_of(ActionMailer::Base).to receive(:mail) + .with(hash_including(template_name: 'idv_please_call')) + .and_call_original + + 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 let(:current_address_matches_id) { false } let!(:enrollment) do @@ -1295,29 +1319,6 @@ def expect_email_body_to_have_help_and_contact_links end end - describe '#in_person_please_call' do - let(:mail) do - UserMailer.with(user: user, email_address: email_address).in_person_please_call( - enrollment: enrollment, - visited_location_name: visited_location_name, - ) - end - - it_behaves_like 'a system email' - it_behaves_like 'an email that respects user email locale preference' - - context 'when the keyword argument visited_location_name is missing' do - let(:mail) do - UserMailer.with(user: user, email_address: email_address).in_person_please_call( - enrollment: enrollment, - ) - end - it 'sends the email successfully' do - mail.deliver_later - end - end - end - describe '#in_person_completion_survey' do let(:mail) do UserMailer.with(user: user, email_address: email_address).in_person_completion_survey diff --git a/spec/support/mailer_helper.rb b/spec/support/mailer_helper.rb index eb264b0adca..2926e7aa6a9 100644 --- a/spec/support/mailer_helper.rb +++ b/spec/support/mailer_helper.rb @@ -15,26 +15,116 @@ 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 = find_sent_email(to:, subject:, body:) + + error_message = <<~ERROR + Unable to find email matching args: + to: #{to} + subject: #{subject} + body: #{body} + 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 = 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? + to_matches = to.nil? || sent_mail.to.include?(to) + subject_matches = subject.nil? || sent_mail.subject == subject + body_matches = body.nil? || begin delivered_body = sent_mail.text_part.decoded.squish - body.to_a.each do |expected_body| - next unless delivered_body.include?(expected_body) + body.to_a.all? do |expected_substring| + delivered_body.include?(expected_substring) end end - true + + to_matches && subject_matches && body_matches end error_message = <<~ERROR - Unable to find email matching args: + Found an email matching the below (but shouldn't have): to: #{to} subject: #{subject} body: #{body} Sent mails: #{ActionMailer::Base.deliveries} ERROR - expect(email).to_not be(nil), error_message + + 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 + + body.to_a.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? || sent_mail.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 && body.to_a.map do |substring| + found = body_text.include?(substring) + "- #{substring} (#{found ? 'found' : 'not found'})" + end + + 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}", + ].compact.join("\n") + end + + def summarize_all_deliveries(query) + ActionMailer::Base.deliveries.map do |email| + summary = summarize_delivery(delivery:, **query) + [ + "- #{summary.lines.first.strip}", + *summary.lines.drop(1).map { |l| l.strip.indent(2) }, + ].join("\n") + end.join("\n") end end diff --git a/spec/support/shared_examples/mailer_preview.rb b/spec/support/shared_examples/mailer_preview.rb index 04918203c96..4f359c1f43b 100644 --- a/spec/support/shared_examples/mailer_preview.rb +++ b/spec/support/shared_examples/mailer_preview.rb @@ -1,10 +1,10 @@ -RSpec.shared_examples 'a mailer preview' do +RSpec.shared_examples 'a mailer preview' do |preview_methods_that_can_be_missing: []| let(:mailer_class) { described_class.class_name.gsub(/Preview$/, '').constantize } it 'has a preview method for each mailer method' do mailer_methods = mailer_class.instance_methods(false) preview_methods = described_class.instance_methods(false) - expect(mailer_methods - preview_methods).to be_empty + expect(mailer_methods - preview_methods).to eql(preview_methods_that_can_be_missing) end described_class.instance_methods(false).each do |mailer_method|