From 6a2b14fbba4aa6436b814956622215d80829f753 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Fri, 9 Oct 2026 09:30:32 +0200 Subject: [PATCH] spec: extract EmailHelpers for delivered-email assertions --- .../workshop_invitation_controller_spec.rb | 15 +++-------- spec/shared_contexts/email_helpers_spec.rb | 25 +++++++++++++++++++ spec/spec_helper.rb | 1 + spec/support/helpers/email_helpers.rb | 18 +++++++++++++ 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 spec/shared_contexts/email_helpers_spec.rb create mode 100644 spec/support/helpers/email_helpers.rb diff --git a/spec/controllers/workshop_invitation_controller_spec.rb b/spec/controllers/workshop_invitation_controller_spec.rb index 2e8a3762e..7ab257b3c 100644 --- a/spec/controllers/workshop_invitation_controller_spec.rb +++ b/spec/controllers/workshop_invitation_controller_spec.rb @@ -8,13 +8,6 @@ before { login(member) } - # The invitation mailer sends multipart/mixed > multipart/alternative > text/html; - # the root body is empty, so read the nested html part. - def html_body(mail) - parts = mail.parts.flat_map { |part| part.multipart? ? part.parts : [part] } - parts.find { |part| part.content_type.match?('text/html') }&.body&.decoded - end - describe 'GET #show' do it 'returns http success' do get :show, params: { id: invitation.token } @@ -192,16 +185,16 @@ def html_body(mail) it 'emails the promoted member a confirmation they are attending' do post :reject, params: { id: invitation.token } - mail = ActionMailer::Base.deliveries.find { |m| m.to.include?(waitlisted_member.email) } + mail = delivered_emails_to(waitlisted_member).last expect(mail).not_to be_nil - expect(html_body(mail)).to include('been confirmed') + expect(email_html(mail)).to include('been confirmed') end it 'sends the promotion variant of the email (waiting-list flag set)' do post :reject, params: { id: invitation.token } - mail = ActionMailer::Base.deliveries.find { |m| m.to.include?(waitlisted_member.email) } - expect(html_body(mail)).to include('A spot became available and your attendance has now been confirmed!') + expect(email_html(delivered_emails_to(waitlisted_member).last)) + .to include('A spot became available and your attendance has now been confirmed!') end it 'does not email anyone else (the rejecting member gets no promotion copy)' do diff --git a/spec/shared_contexts/email_helpers_spec.rb b/spec/shared_contexts/email_helpers_spec.rb new file mode 100644 index 000000000..4c061999f --- /dev/null +++ b/spec/shared_contexts/email_helpers_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +# Canary for EmailHelpers — same pattern as the 'with forgery protection +# enforced' canary. It delivers a real invitation email so a change to the +# mailer's part structure or to the test delivery path fails here instead of +# silently weakening every email assertion that uses the helper. +RSpec.describe EmailHelpers do + let(:member) { Fabricate(:member) } + let(:workshop) { Fabricate(:workshop) } + let(:invitation) { Fabricate(:workshop_invitation, workshop:, member:) } + + it 'delivered_emails_to filters delivered mail by recipient' do + WorkshopInvitationMailer.attending(workshop, member, invitation, false).deliver_now + + expect(delivered_emails_to(member)).not_to be_empty + expect(delivered_emails_to('nobody@example.com')).to be_empty + end + + it 'email_html reads the waiting-list html copy from the nested part' do + WorkshopInvitationMailer.attending(workshop, member, invitation, true).deliver_now + + expect(email_html(delivered_emails_to(member).last)) + .to include('A spot became available and your attendance has now been confirmed!') + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88610b6ad..61ef72081 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,6 +50,7 @@ RSpec.configure do |config| config.include ActiveJob::TestHelper config.include ApplicationHelper + config.include EmailHelpers config.include LoginHelpers config.include ActiveSupport::Testing::TimeHelpers config.include SelectFromChosen, type: :feature diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb new file mode 100644 index 000000000..7e0e8102d --- /dev/null +++ b/spec/support/helpers/email_helpers.rb @@ -0,0 +1,18 @@ +module EmailHelpers + # Mails the test-delivery path collected for a recipient (a Member or an + # address). The test queue adapter is delayed_job with Delayed::Worker.delay_jobs + # set to false in test, so deliver_later delivers immediately and mail lands in + # ActionMailer::Base.deliveries. + def delivered_emails_to(recipient) + address = recipient.respond_to?(:email) ? recipient.email : recipient + ActionMailer::Base.deliveries.select { |mail| mail.to.include?(address) } + end + + # Decoded text/html body of a delivered Mail::Message. The invitation mailers + # send multipart/mixed > multipart/alternative > text/html, so the root + # mail.body.decoded is empty and the html part must be read directly. + def email_html(mail) + parts = mail.parts.flat_map { |part| part.multipart? ? part.parts : [part] } + parts.find { |part| part.content_type.match?('text/html') }&.body&.decoded + end +end