Skip to content

Commit 08e210f

Browse files
authored
Merge pull request #2745 from odlp/pr-verify-job-perform-signature
Verify ActiveJob job signature matches arguments
2 parents e160cbd + 9a52474 commit 08e210f

File tree

5 files changed

+168
-32
lines changed

5 files changed

+168
-32
lines changed

lib/rspec/rails/matchers/active_job.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def thrice
7171
end
7272

7373
def failure_message
74+
return @failure_message if defined?(@failure_message)
75+
7476
"expected to #{self.class::FAILURE_MESSAGE_EXPECTATION_ACTION} #{base_message}".tap do |msg|
7577
if @unmatching_jobs.any?
7678
msg << "\nQueued jobs:"
@@ -109,6 +111,12 @@ def check(jobs)
109111
false
110112
end
111113
end
114+
115+
if (signature_mismatch = detect_args_signature_mismatch(@matching_jobs))
116+
@failure_message = signature_mismatch
117+
return false
118+
end
119+
112120
@matching_jobs_count = @matching_jobs.size
113121

114122
case @expectation_type
@@ -152,6 +160,27 @@ def arguments_match?(job)
152160
end
153161
end
154162

163+
def detect_args_signature_mismatch(jobs)
164+
jobs.each do |job|
165+
args = deserialize_arguments(job)
166+
167+
if (signature_mismatch = check_args_signature_mismatch(job.fetch(:job), :perform, args))
168+
return signature_mismatch
169+
end
170+
end
171+
172+
nil
173+
end
174+
175+
def check_args_signature_mismatch(job_class, job_method, args)
176+
signature = Support::MethodSignature.new(job_class.public_instance_method(job_method))
177+
verifier = Support::StrictSignatureVerifier.new(signature, args)
178+
179+
unless verifier.valid?
180+
"Incorrect arguments passed to #{job_class.name}: #{verifier.error_message}"
181+
end
182+
end
183+
155184
def queue_match?(job)
156185
return true unless @queue
157186

lib/rspec/rails/matchers/have_enqueued_mail.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def matches?(block)
4141
end
4242

4343
def failure_message
44+
return @failure_message if defined?(@failure_message)
45+
4446
"expected to enqueue #{base_message}".tap do |msg|
4547
msg << "\n#{unmatching_mail_jobs_message}" if unmatching_mail_jobs.any?
4648
end
@@ -89,6 +91,22 @@ def arguments_match?(job)
8991
super(job)
9092
end
9193

94+
def detect_args_signature_mismatch(jobs)
95+
return if @method_name.nil?
96+
97+
mailer_class = mailer_class_name.constantize
98+
99+
jobs.each do |job|
100+
mailer_args = extract_args_without_parameterized_params(job)
101+
102+
if (signature_mismatch = check_args_signature_mismatch(mailer_class, @method_name, mailer_args))
103+
return signature_mismatch
104+
end
105+
end
106+
107+
nil
108+
end
109+
92110
def base_mailer_args
93111
[mailer_class_name, @method_name.to_s, MAILER_JOB_METHOD]
94112
end
@@ -157,6 +175,19 @@ def deserialize_arguments(job)
157175
end
158176
end
159177

178+
def extract_args_without_parameterized_params(job)
179+
args = deserialize_arguments(job)
180+
mailer_args = args - base_mailer_args
181+
182+
if parameterized_mail?(job)
183+
mailer_args = mailer_args[1..-1] # ignore parameterized params
184+
elsif mailer_args.last.is_a?(Hash) && mailer_args.last.key?(:args)
185+
mailer_args = args.last[:args]
186+
end
187+
188+
mailer_args
189+
end
190+
160191
def legacy_mail?(job)
161192
RSpec::Rails::FeatureCheck.has_action_mailer_legacy_delivery_job? && job[:job] <= ActionMailer::DeliveryJob
162193
end

spec/rspec/rails/matchers/active_job_spec.rb

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ def self.name; "LoggingJob"; end
6464
end
6565
end
6666

67+
let(:two_args_job) do
68+
Class.new(ActiveJob::Base) do
69+
def perform(one, two); end
70+
def self.name; "TwoArgsJob"; end
71+
end
72+
end
73+
74+
let(:keyword_args_job) do
75+
Class.new(ActiveJob::Base) do
76+
def perform(one:, two:); end
77+
def self.name; "KeywordArgsJob"; end
78+
end
79+
end
80+
6781
before do
6882
ActiveJob::Base.queue_adapter = :test
6983
end
@@ -138,7 +152,7 @@ def perform; raise StandardError; end
138152
it "fails when job is not enqueued" do
139153
expect {
140154
expect { }.to have_enqueued_job
141-
}.to raise_error(/expected to enqueue exactly 1 jobs, but enqueued 0/)
155+
}.to fail_with(/expected to enqueue exactly 1 jobs, but enqueued 0/)
142156
end
143157

144158
it "fails when too many jobs enqueued" do
@@ -147,20 +161,20 @@ def perform; raise StandardError; end
147161
heavy_lifting_job.perform_later
148162
heavy_lifting_job.perform_later
149163
}.to have_enqueued_job.exactly(1)
150-
}.to raise_error(/expected to enqueue exactly 1 jobs, but enqueued 2/)
164+
}.to fail_with(/expected to enqueue exactly 1 jobs, but enqueued 2/)
151165
end
152166

153167
it "reports correct number in fail error message" do
154168
heavy_lifting_job.perform_later
155169
expect {
156170
expect { }.to have_enqueued_job.exactly(1)
157-
}.to raise_error(/expected to enqueue exactly 1 jobs, but enqueued 0/)
171+
}.to fail_with(/expected to enqueue exactly 1 jobs, but enqueued 0/)
158172
end
159173

160174
it "fails when negated and job is enqueued" do
161175
expect {
162176
expect { heavy_lifting_job.perform_later }.not_to have_enqueued_job
163-
}.to raise_error(/expected not to enqueue at least 1 jobs, but enqueued 1/)
177+
}.to fail_with(/expected not to enqueue at least 1 jobs, but enqueued 1/)
164178
end
165179

166180
it "fails when negated and several jobs enqueued" do
@@ -169,7 +183,7 @@ def perform; raise StandardError; end
169183
heavy_lifting_job.perform_later
170184
heavy_lifting_job.perform_later
171185
}.not_to have_enqueued_job
172-
}.to raise_error(/expected not to enqueue at least 1 jobs, but enqueued 2/)
186+
}.to fail_with(/expected not to enqueue at least 1 jobs, but enqueued 2/)
173187
end
174188

175189
it "passes with job name" do
@@ -224,7 +238,7 @@ def perform; raise StandardError; end
224238
it "generates failure message with at least hint" do
225239
expect {
226240
expect { }.to have_enqueued_job.at_least(:once)
227-
}.to raise_error(/expected to enqueue at least 1 jobs, but enqueued 0/)
241+
}.to fail_with(/expected to enqueue at least 1 jobs, but enqueued 0/)
228242
end
229243

230244
it "generates failure message with at most hint" do
@@ -233,7 +247,7 @@ def perform; raise StandardError; end
233247
hello_job.perform_later
234248
hello_job.perform_later
235249
}.to have_enqueued_job.at_most(:once)
236-
}.to raise_error(/expected to enqueue at most 1 jobs, but enqueued 2/)
250+
}.to fail_with(/expected to enqueue at most 1 jobs, but enqueued 2/)
237251
end
238252

239253
it "passes with provided queue name as string" do
@@ -277,7 +291,7 @@ def perform; raise StandardError; end
277291
travel_to time do
278292
expect {
279293
expect { hello_job.set(wait: 5).perform_later }.to have_enqueued_job.at(time + 5)
280-
}.to raise_error(/expected to enqueue exactly 1 jobs/)
294+
}.to fail_with(/expected to enqueue exactly 1 jobs/)
281295
end
282296
end
283297

@@ -301,7 +315,7 @@ def perform; raise StandardError; end
301315
expect {
302316
hello_job.perform_later
303317
}.to have_enqueued_job.at(date)
304-
}.to raise_error(/expected to enqueue exactly 1 jobs, at .+ but enqueued 0/)
318+
}.to fail_with(/expected to enqueue exactly 1 jobs, at .+ but enqueued 0/)
305319
end
306320

307321
it "has an enqueued job when not providing at and there is a wait" do
@@ -324,6 +338,22 @@ def perform; raise StandardError; end
324338
}.to have_enqueued_job.with(42, "David")
325339
end
326340

341+
it "fails if the arguments do not match the job's signature" do
342+
expect {
343+
expect {
344+
two_args_job.perform_later(1)
345+
}.to have_enqueued_job.with(1)
346+
}.to fail_with(/Incorrect arguments passed to TwoArgsJob: Wrong number of arguments/)
347+
end
348+
349+
it "fails if the job's signature/arguments are mismatched keyword/positional arguments" do
350+
expect {
351+
expect {
352+
keyword_args_job.perform_later(1, 2)
353+
}.to have_enqueued_job.with(1, 2)
354+
}.to fail_with(/Incorrect arguments passed to KeywordArgsJob: Missing required keyword arguments/)
355+
end
356+
327357
it "passes with provided arguments containing global id object" do
328358
global_id_object = GlobalIdModel.new("42")
329359

@@ -348,7 +378,7 @@ def perform; raise StandardError; end
348378
expect {
349379
hello_job.perform_later(1)
350380
}.to have_enqueued_job(hello_job).with(42).on_queue("low").at(date).exactly(2).times
351-
}.to raise_error(message)
381+
}.to fail_with(message)
352382
end
353383

354384
it "throws descriptive error when no test adapter set" do
@@ -454,15 +484,31 @@ def perform; raise StandardError; end
454484
it "fails when job is not enqueued" do
455485
expect {
456486
expect(heavy_lifting_job).to have_been_enqueued
457-
}.to raise_error(/expected to enqueue exactly 1 jobs, but enqueued 0/)
487+
}.to fail_with(/expected to enqueue exactly 1 jobs, but enqueued 0/)
488+
end
489+
490+
it "fails if the arguments do not match the job's signature" do
491+
two_args_job.perform_later(1)
492+
493+
expect {
494+
expect(two_args_job).to have_been_enqueued.with(1)
495+
}.to fail_with(/Incorrect arguments passed to TwoArgsJob: Wrong number of arguments/)
496+
end
497+
498+
it "fails if the job's signature/arguments are mismatched keyword/positional arguments" do
499+
keyword_args_job.perform_later(1, 2)
500+
501+
expect {
502+
expect(keyword_args_job).to have_been_enqueued.with(1, 2)
503+
}.to fail_with(/Incorrect arguments passed to KeywordArgsJob: Missing required keyword arguments/)
458504
end
459505

460506
it "fails when negated and several jobs enqueued" do
461507
heavy_lifting_job.perform_later
462508
heavy_lifting_job.perform_later
463509
expect {
464510
expect(heavy_lifting_job).not_to have_been_enqueued
465-
}.to raise_error(/expected not to enqueue at least 1 jobs, but enqueued 2/)
511+
}.to fail_with(/expected not to enqueue at least 1 jobs, but enqueued 2/)
466512
end
467513

468514
it "accepts composable matchers as an at date" do
@@ -511,7 +557,7 @@ def perform; raise StandardError; end
511557
it "fails when job is not performed" do
512558
expect {
513559
expect { }.to have_performed_job
514-
}.to raise_error(/expected to perform exactly 1 jobs, but performed 0/)
560+
}.to fail_with(/expected to perform exactly 1 jobs, but performed 0/)
515561
end
516562

517563
it "fails when too many jobs performed" do
@@ -520,20 +566,20 @@ def perform; raise StandardError; end
520566
heavy_lifting_job.perform_later
521567
heavy_lifting_job.perform_later
522568
}.to have_performed_job.exactly(1)
523-
}.to raise_error(/expected to perform exactly 1 jobs, but performed 2/)
569+
}.to fail_with(/expected to perform exactly 1 jobs, but performed 2/)
524570
end
525571

526572
it "reports correct number in fail error message" do
527573
heavy_lifting_job.perform_later
528574
expect {
529575
expect { }.to have_performed_job.exactly(1)
530-
}.to raise_error(/expected to perform exactly 1 jobs, but performed 0/)
576+
}.to fail_with(/expected to perform exactly 1 jobs, but performed 0/)
531577
end
532578

533579
it "fails when negated and job is performed" do
534580
expect {
535581
expect { heavy_lifting_job.perform_later }.not_to have_performed_job
536-
}.to raise_error(/expected not to perform exactly 1 jobs, but performed 1/)
582+
}.to fail_with(/expected not to perform exactly 1 jobs, but performed 1/)
537583
end
538584

539585
it "passes with job name" do
@@ -588,7 +634,7 @@ def perform; raise StandardError; end
588634
it "generates failure message with at least hint" do
589635
expect {
590636
expect { }.to have_performed_job.at_least(:once)
591-
}.to raise_error(/expected to perform at least 1 jobs, but performed 0/)
637+
}.to fail_with(/expected to perform at least 1 jobs, but performed 0/)
592638
end
593639

594640
it "generates failure message with at most hint" do
@@ -597,7 +643,7 @@ def perform; raise StandardError; end
597643
hello_job.perform_later
598644
hello_job.perform_later
599645
}.to have_performed_job.at_most(:once)
600-
}.to raise_error(/expected to perform at most 1 jobs, but performed 2/)
646+
}.to fail_with(/expected to perform at most 1 jobs, but performed 2/)
601647
end
602648

603649
it "passes with provided queue name as string" do
@@ -649,7 +695,7 @@ def perform; raise StandardError; end
649695
expect {
650696
hello_job.perform_later(1)
651697
}.to have_performed_job(hello_job).with(42).on_queue("low").at(date).exactly(2).times
652-
}.to raise_error(message)
698+
}.to fail_with(message)
653699
end
654700

655701
it "throws descriptive error when no test adapter set" do
@@ -734,7 +780,7 @@ def perform; raise StandardError; end
734780
it "fails when job is not performed" do
735781
expect {
736782
expect(heavy_lifting_job).to have_been_performed
737-
}.to raise_error(/expected to perform exactly 1 jobs, but performed 0/)
783+
}.to fail_with(/expected to perform exactly 1 jobs, but performed 0/)
738784
end
739785
end
740786

0 commit comments

Comments
 (0)