Skip to content

Fix have_enqueued_mail for Rails 7 and argument matching #2546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 10, 2022
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Bug Fixes:

* Properly name params in controller and request spec templates when
using the `--model-name` parameter. (@kenzo-tanaka, #2534)
* Fix parameter matching with mail delivery job and
ActionMailer::MailDeliveryJob. (Fabio Napoleoni, #2516, #2546)
* Fix Rails 7 `have_enqueued_mail` compatibility (Mikael Henriksson, #2537, #2546)

### 5.0.2 / 2021-08-14
[Full Changelog](https://github.com/rspec/rspec-rails/compare/v5.0.1...v5.0.2)
Expand Down
88 changes: 88 additions & 0 deletions features/matchers/have_enqueued_mail_matcher.feature
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,91 @@ Feature: have_enqueued_mail matcher
"""
When I run `rspec spec/mailers/user_mailer_spec.rb`
Then the examples should all pass

Scenario: Checking mailer arguments
Given a file named "app/mailers/my_mailer.rb" with:
"""ruby
class MyMailer < ApplicationMailer

def signup(user = nil)
@user = user

mail to: "[email protected]"
end
end
"""
Given a file named "spec/mailers/my_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe MyMailer do
it "matches with enqueued mailer" do
ActiveJob::Base.queue_adapter = :test
# Works with plain args
expect {
MyMailer.signup('user').deliver_later
}.to have_enqueued_mail(MyMailer, :signup).with('user')
end
end
"""
When I run `rspec spec/mailers/my_mailer_spec.rb`
Then the examples should all pass

Scenario: Parameterize the mailer
Given a file named "app/mailers/my_mailer.rb" with:
"""ruby
class MyMailer < ApplicationMailer

def signup
@foo = params[:foo]

mail to: "[email protected]"
end
end
"""
Given a file named "spec/mailers/my_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe MyMailer do
it "matches with enqueued mailer" do
ActiveJob::Base.queue_adapter = :test
# Works with named parameters
expect {
MyMailer.with(foo: 'bar').signup.deliver_later
}.to have_enqueued_mail(MyMailer, :signup).with(foo: 'bar')
end
end
"""
When I run `rspec spec/mailers/my_mailer_spec.rb`
Then the examples should all pass

Scenario: Parameterize and pass an argument to the mailer
Given a file named "app/mailers/my_mailer.rb" with:
"""ruby
class MyMailer < ApplicationMailer

def signup(user)
@user = user
@foo = params[:foo]

mail to: "[email protected]"
end
end
"""
Given a file named "spec/mailers/my_mailer_spec.rb" with:
"""ruby
require "rails_helper"

RSpec.describe MyMailer do
it "matches with enqueued mailer" do
ActiveJob::Base.queue_adapter = :test
# Works also with both, named parameters match first argument
expect {
MyMailer.with(foo: 'bar').signup('user').deliver_later
}.to have_enqueued_mail(MyMailer, :signup).with({foo: 'bar'}, 'user')
end
end
"""
When I run `rspec spec/mailers/my_mailer_spec.rb`
Then the examples should all pass
6 changes: 5 additions & 1 deletion lib/rspec/rails/feature_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ def has_action_cable_testing?
end

def has_action_mailer_parameterized?
has_action_mailer? && defined?(::ActionMailer::Parameterized)
has_action_mailer? && defined?(::ActionMailer::Parameterized::DeliveryJob)
end

def has_action_mailer_unified_delivery?
has_action_mailer? && defined?(::ActionMailer::MailDeliveryJob)
end

def has_action_mailer_legacy_delivery_job?
defined?(ActionMailer::DeliveryJob)
end

def has_action_mailbox?
defined?(::ActionMailbox)
end
Expand Down
32 changes: 27 additions & 5 deletions lib/rspec/rails/matchers/have_enqueued_mail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
module RSpec
module Rails
module Matchers
# rubocop: disable Metrics/ClassLength
# Matcher class for `have_enqueued_mail`. Should not be instantiated directly.
#
# @private
Expand Down Expand Up @@ -76,7 +77,7 @@ def job_match?(job)
def arguments_match?(job)
@args =
if @mail_args.any?
base_mailer_args + @mail_args
base_mailer_args + process_arguments(job, @mail_args)
elsif @mailer_class && @method_name
base_mailer_args + [any_args]
elsif @mailer_class
Expand All @@ -88,12 +89,31 @@ def arguments_match?(job)
super(job)
end

def process_arguments(job, given_mail_args)
# Old matcher behavior working with all builtin classes but ActionMailer::MailDeliveryJob
return given_mail_args unless defined?(ActionMailer::MailDeliveryJob) && job[:job] <= ActionMailer::MailDeliveryJob

# If matching args starts with a hash and job instance has params match with them
if given_mail_args.first.is_a?(Hash) && job[:args][3]['params'].present?
[hash_including(params: given_mail_args[0], args: given_mail_args.drop(1))]
else
[hash_including(args: given_mail_args)]
end
end

def base_mailer_args
[mailer_class_name, @method_name.to_s, MAILER_JOB_METHOD]
end

def yield_mail_args(block)
proc { |*job_args| block.call(*(job_args - base_mailer_args)) }
proc do |*job_args|
mailer_args = job_args - base_mailer_args
if mailer_args.first.is_a?(Hash)
block.call(*mailer_args.first[:args])
else
block.call(*mailer_args)
end
end
end

def check_active_job_adapter
Expand All @@ -120,8 +140,8 @@ def unmatching_mail_jobs_message

def mail_job_message(job)
mailer_method = job[:args][0..1].join('.')

mailer_args = job[:args][3..-1]
mailer_args = deserialize_arguments(job)[3..-1]
mailer_args = mailer_args.first[:args] if unified_mail?(job)
msg_parts = []
msg_parts << "with #{mailer_args}" if mailer_args.any?
msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
Expand All @@ -131,7 +151,7 @@ def mail_job_message(job)
end

def legacy_mail?(job)
job[:job] <= ActionMailer::DeliveryJob
RSpec::Rails::FeatureCheck.has_action_mailer_legacy_delivery_job? && job[:job] <= ActionMailer::DeliveryJob
end

def parameterized_mail?(job)
Expand All @@ -142,6 +162,8 @@ def unified_mail?(job)
RSpec::Rails::FeatureCheck.has_action_mailer_unified_delivery? && job[:job] <= ActionMailer::MailDeliveryJob
end
end
# rubocop: enable Metrics/ClassLength

# @api public
# Passes if an email has been enqueued inside block.
# May chain with to specify expected arguments.
Expand Down
1 change: 1 addition & 0 deletions spec/rspec/rails/example/system_example_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module RSpec::Rails

describe '#after' do
it 'sets the :extra_failure_lines metadata to an array of STDOUT lines' do
allow(Capybara::Session).to receive(:instance_created?).and_return(true)
group = RSpec::Core::ExampleGroup.describe do
include SystemExampleGroup

Expand Down
25 changes: 17 additions & 8 deletions spec/rspec/rails/matchers/have_enqueued_mail_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def test_email; end
def email_with_args(arg1, arg2); end
end

class DeliveryJobSubClass < ActionMailer::DeliveryJob
if RSpec::Rails::FeatureCheck.has_action_mailer_legacy_delivery_job?
class DeliveryJobSubClass < ActionMailer::DeliveryJob
end
else
class DeliveryJobSubClass < ActionMailer::MailDeliveryJob
end
end

class UnifiedMailerWithDeliveryJobSubClass < ActionMailer::Base
Expand Down Expand Up @@ -393,18 +398,22 @@ def self.name; "NonMailerJob"; end
}.to have_enqueued_mail(UnifiedMailer, :test_email).and have_enqueued_mail(UnifiedMailer, :email_with_args)
end

it "passes with provided argument matchers" do
it "matches arguments when mailer has only args" do
expect {
UnifiedMailer.email_with_args(1, 2).deliver_later
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with(1, 2)
end

it "matches arguments when mailer is parameterized" do
expect {
UnifiedMailer.with('foo' => 'bar').test_email.deliver_later
}.to have_enqueued_mail(UnifiedMailer, :test_email).with(
a_hash_including(params: {'foo' => 'bar'})
)
}.to have_enqueued_mail(UnifiedMailer, :test_email).with('foo' => 'bar')
end

it "matches arguments when mixing parameterized and non-parameterized emails" do
expect {
UnifiedMailer.with('foo' => 'bar').email_with_args(1, 2).deliver_later
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with(
a_hash_including(params: {'foo' => 'bar'}, args: [1, 2])
)
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with({'foo' => 'bar'}, 1, 2)
end

it "passes when using a mailer with `delivery_job` set to a sub class of `ActionMailer::DeliveryJob`" do
Expand Down