Skip to content

Commit 5fe75e7

Browse files
authored
Merge pull request #2570 from rspec/fix-ruby-31-enqueue_mail-matcher
Fix mailer argument deserialisation for 6.1 on Ruby 3.1
2 parents 0702276 + b22a08b commit 5fe75e7

File tree

5 files changed

+54
-51
lines changed

5 files changed

+54
-51
lines changed

.rubocop_todo.yml

-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,3 @@ Layout/LineLength:
99
# Over time we'd like to get this down, but this is what we're at now.
1010
Metrics/MethodLength:
1111
Max: 43 # default: 10
12-
13-
Metrics/ClassLength:
14-
Exclude:
15-
- lib/rspec/rails/matchers/have_enqueued_mail.rb

features/matchers/have_enqueued_mail_matcher.feature

+4-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Feature: have_enqueued_mail matcher
6868
When I run `rspec spec/mailers/my_mailer_spec.rb`
6969
Then the examples should all pass
7070

71+
@rails_post_6
7172
Scenario: Parameterize the mailer
7273
Given a file named "app/mailers/my_mailer.rb" with:
7374
"""ruby
@@ -90,13 +91,14 @@ Feature: have_enqueued_mail matcher
9091
# Works with named parameters
9192
expect {
9293
MyMailer.with(foo: 'bar').signup.deliver_later
93-
}.to have_enqueued_mail(MyMailer, :signup).with(foo: 'bar')
94+
}.to have_enqueued_mail(MyMailer, :signup).with(a_hash_including(params: {foo: 'bar'}))
9495
end
9596
end
9697
"""
9798
When I run `rspec spec/mailers/my_mailer_spec.rb`
9899
Then the examples should all pass
99100

101+
@rails_post_6
100102
Scenario: Parameterize and pass an argument to the mailer
101103
Given a file named "app/mailers/my_mailer.rb" with:
102104
"""ruby
@@ -120,7 +122,7 @@ Feature: have_enqueued_mail matcher
120122
# Works also with both, named parameters match first argument
121123
expect {
122124
MyMailer.with(foo: 'bar').signup('user').deliver_later
123-
}.to have_enqueued_mail(MyMailer, :signup).with({foo: 'bar'}, 'user')
125+
}.to have_enqueued_mail(MyMailer, :signup).with(params: {foo: 'bar'}, args: ['user'])
124126
end
125127
end
126128
"""

lib/rspec/rails/feature_check.rb

-4
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ def has_action_mailbox?
4343
defined?(::ActionMailbox)
4444
end
4545

46-
def ruby_3_1?
47-
RUBY_VERSION >= "3.1"
48-
end
49-
5046
def type_metatag(type)
5147
"type: :#{type}"
5248
end

lib/rspec/rails/matchers/have_enqueued_mail.rb

+31-36
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "rspec/mocks/argument_matchers"
55
require "rspec/rails/matchers/active_job"
66

7+
# rubocop: disable Metrics/ClassLength
78
module RSpec
89
module Rails
910
module Matchers
@@ -76,7 +77,7 @@ def job_match?(job)
7677
def arguments_match?(job)
7778
@args =
7879
if @mail_args.any?
79-
base_mailer_args + process_arguments(job, @mail_args)
80+
base_mailer_args + @mail_args
8081
elsif @mailer_class && @method_name
8182
base_mailer_args + [any_args]
8283
elsif @mailer_class
@@ -88,38 +89,12 @@ def arguments_match?(job)
8889
super(job)
8990
end
9091

91-
def process_arguments(job, given_mail_args)
92-
# Old matcher behavior working with all builtin classes but ActionMailer::MailDeliveryJob
93-
return given_mail_args if use_given_mail_args?(job)
94-
95-
# If matching args starts with a hash and job instance has params match with them
96-
if given_mail_args.first.is_a?(Hash) && job[:args][3]['params'].present?
97-
[hash_including(params: given_mail_args[0], args: given_mail_args.drop(1))]
98-
else
99-
[hash_including(args: given_mail_args)]
100-
end
101-
end
102-
103-
def use_given_mail_args?(job)
104-
return true if FeatureCheck.has_action_mailer_parameterized? && job[:job] <= ActionMailer::Parameterized::DeliveryJob
105-
return false if FeatureCheck.ruby_3_1?
106-
107-
!(FeatureCheck.has_action_mailer_unified_delivery? && job[:job] <= ActionMailer::MailDeliveryJob)
108-
end
109-
11092
def base_mailer_args
11193
[mailer_class_name, @method_name.to_s, MAILER_JOB_METHOD]
11294
end
11395

11496
def yield_mail_args(block)
115-
proc do |*job_args|
116-
mailer_args = job_args - base_mailer_args
117-
if mailer_args.first.is_a?(Hash)
118-
block.call(*mailer_args.first[:args])
119-
else
120-
block.call(*mailer_args)
121-
end
122-
end
97+
proc { |*job_args| block.call(*(job_args - base_mailer_args)) }
12398
end
12499

125100
def check_active_job_adapter
@@ -145,22 +120,41 @@ def unmatching_mail_jobs_message
145120
end
146121

147122
def mail_job_message(job)
148-
mailer_method = job[:args][0..1].join('.')
149-
mailer_args = deserialize_arguments(job)[3..-1]
150-
mailer_args = mailer_args.first[:args] if unified_mail?(job)
123+
job_args = deserialize_arguments(job)
124+
125+
mailer_method = job_args[0..1].join('.')
126+
mailer_args = job_args[3..-1]
127+
151128
msg_parts = []
152-
display_args = display_mailer_args(mailer_args)
153-
msg_parts << "with #{display_args}" if display_args.any?
129+
msg_parts << "with #{mailer_args}" if mailer_args.any?
154130
msg_parts << "on queue #{job[:queue]}" if job[:queue] && job[:queue] != 'mailers'
155131
msg_parts << "at #{Time.at(job[:at])}" if job[:at]
156132

157133
"#{mailer_method} #{msg_parts.join(', ')}".strip
158134
end
159135

160-
def display_mailer_args(mailer_args)
161-
return mailer_args unless mailer_args.first.is_a?(Hash) && mailer_args.first.key?(:args)
136+
# Ruby 3.1 changed how params were serialized on Rails 6.1
137+
# so we override the active job implementation and customise it here.
138+
def deserialize_arguments(job)
139+
args = super
140+
141+
return args unless Hash === args.last
142+
143+
hash = args.pop
162144

163-
mailer_args.first[:args]
145+
if hash.key?("_aj_ruby2_keywords")
146+
keywords = hash["_aj_ruby2_keywords"]
147+
148+
original_hash = keywords.each_with_object({}) { |keyword, new_hash| new_hash[keyword.to_sym] = hash[keyword] }
149+
150+
args + [original_hash]
151+
elsif hash.key?(:args) && hash.key?(:params)
152+
args + [hash]
153+
elsif hash.key?(:args)
154+
args + hash[:args]
155+
else
156+
args + [hash]
157+
end
164158
end
165159

166160
def legacy_mail?(job)
@@ -230,3 +224,4 @@ def have_enqueued_mail(mailer_class = nil, mail_method_name = nil)
230224
end
231225
end
232226
end
227+
# rubocop: enable Metrics/ClassLength

spec/rspec/rails/matchers/have_enqueued_mail_spec.rb

+19-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
require "action_mailer"
55
require "rspec/rails/matchers/have_enqueued_mail"
66

7+
class GlobalIDArgument
8+
include GlobalID::Identification
9+
def id; 1; end
10+
def to_global_id(options = {}); super(options.merge(app: 'rspec-rails')); end
11+
end
12+
713
class TestMailer < ActionMailer::Base
814
def test_email; end
915
def email_with_args(arg1, arg2); end
@@ -404,16 +410,24 @@ def self.name; "NonMailerJob"; end
404410
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with(1, 2)
405411
end
406412

407-
it "matches arguments when mailer is parameterized" do
413+
it "passes with provided argument matchers" do
408414
expect {
409415
UnifiedMailer.with('foo' => 'bar').test_email.deliver_later
410-
}.to have_enqueued_mail(UnifiedMailer, :test_email).with('foo' => 'bar')
411-
end
416+
}.to have_enqueued_mail(UnifiedMailer, :test_email).with(
417+
a_hash_including(params: {'foo' => 'bar'})
418+
)
412419

413-
it "matches arguments when mixing parameterized and non-parameterized emails" do
414420
expect {
415421
UnifiedMailer.with('foo' => 'bar').email_with_args(1, 2).deliver_later
416-
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with({'foo' => 'bar'}, 1, 2)
422+
}.to have_enqueued_mail(UnifiedMailer, :email_with_args).with(
423+
a_hash_including(params: {'foo' => 'bar'}, args: [1, 2])
424+
)
425+
end
426+
427+
it "passes when given a global id serialised argument" do
428+
expect {
429+
UnifiedMailer.with(inquiry: GlobalIDArgument.new).test_email.deliver_later
430+
}.to have_enqueued_email(UnifiedMailer, :test_email)
417431
end
418432

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

0 commit comments

Comments
 (0)