sequra-style 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3cb23d52c4f8f874a5aad9ee232ab2b3de754fa4c993fe9a24575072f134d99
4
- data.tar.gz: 0014151bbc9f819fdedf44f7604f2a47fa7e3e320873e501138f5c75886750fb
3
+ metadata.gz: 47e2880848be7928ef68435e320055b4d2003f4b321d172094fa77f123f5139e
4
+ data.tar.gz: d8ef008a1b7ba0477c45a19d52eb965d86a994fe5c7c203c67ecc2941f2ac798
5
5
  SHA512:
6
- metadata.gz: d9ef0de4f838e0be3c4f2413ab7496aa8fa96baf5eb10026cc01a822112d1e51bd0428032b7505de0ef6f3922f77d840c0c9eda081098dfea559d01d3b6641cb
7
- data.tar.gz: cf6170ea43e2aa6f3a03da557bb43927ec72f3ac48bbdefa55f7059426803b96f8439b603f3246df45347505ceb7311e0e0091dc3ea120d9b0c64b06469c8aca
6
+ metadata.gz: 19d7b3760cf692ed282a02b99416f5d3e6f9fc288a8f5ecf8086e40459215f8665c27fc90f336183e42dcc37823668364a9d8503fa8a462b3afc5ad0d68885cb
7
+ data.tar.gz: 17bd16a9f9499060f4ee8e986c0f7a12e32a5a1f5d28ba494b9b5ca2d6ac8e78dda03254ea83d75c8073bc7823c60a738f171dd492e76910269fd41f1f5574da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.19.0](https://github.com/sequra/sequra-style/compare/v1.18.0...v1.19.0) (2026-07-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * [COR-1992] Flag mailer .later stubs in NoSidekiqPerformStubs ([#81](https://github.com/sequra/sequra-style/issues/81)) ([b9ea46e](https://github.com/sequra/sequra-style/commit/b9ea46e97649b40aae30bfa4f6cbbc51fe0c72f0))
9
+
3
10
  ## [1.18.0](https://github.com/sequra/sequra-style/compare/v1.17.0...v1.18.0) (2026-07-28)
4
11
 
5
12
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sequra-style (1.18.0)
4
+ sequra-style (1.19.0)
5
5
  rubocop (~> 1.75)
6
6
  rubocop-performance (~> 1.25)
7
7
  rubocop-rails (~> 2.31)
@@ -2,7 +2,8 @@ module RuboCop
2
2
  module Cop
3
3
  module Sequra
4
4
  # Detects RSpec stubs and spy assertions on Sidekiq enqueue methods
5
- # (`perform_async`, `perform_at`, `perform_in`).
5
+ # (`perform_async`, `perform_at`, `perform_in`) and on the mailer
6
+ # enqueue wrapper (`SomeMailer.later`).
6
7
  #
7
8
  # Stubbing these methods bypasses `Sidekiq.strict_args!` validation,
8
9
  # which lets symbol-keyed hashes (and other non-JSON-native types)
@@ -12,12 +13,19 @@ module RuboCop
12
13
  # rescue-swallowers can mask the failure. This pattern caused a
13
14
  # silent push-notification drop in COR-1923.
14
15
  #
16
+ # `SomeMailer.later` (seQura's `ApplicationMailer.later`) is the
17
+ # standard mailer enqueue path and funnels through
18
+ # `ApplicationMailerJob.perform_async`, so stubbing it bypasses
19
+ # `strict_args!` exactly like stubbing `perform_async` directly.
20
+ # `.later` is only flagged when the stubbed subject is a `*Mailer`
21
+ # constant, so unrelated `.later` methods are left alone.
22
+ #
15
23
  # Use `have_enqueued_sidekiq_job` (or `change(SomeJob.jobs, :size)`)
16
24
  # instead. Both go through Sidekiq's client middleware chain, so
17
25
  # `strict_args!` validates the arguments as it would in production.
18
26
  #
19
27
  # `.and_call_original` is exempt: the stub spies on the call but
20
- # then runs the real `perform_*`, which still exercises the
28
+ # then runs the real method, which still exercises the
21
29
  # serialization contract.
22
30
  #
23
31
  # @example
@@ -33,6 +41,9 @@ module RuboCop
33
41
  # # bad - spy verification on a stubbed enqueue
34
42
  # expect(SomeJob).to have_received(:perform_async).with(id)
35
43
  #
44
+ # # bad - mailer enqueue wrapper bypasses strict_args! too
45
+ # allow(WelcomeMailer).to receive(:later)
46
+ #
36
47
  # # good - exercises strict_args! through Sidekiq's middleware
37
48
  # expect(SomeJob).to have_enqueued_sidekiq_job(id, name: "value")
38
49
  #
@@ -43,17 +54,23 @@ module RuboCop
43
54
  # expect(SomeJob).to receive(:perform_async).and_call_original
44
55
  #
45
56
  class NoSidekiqPerformStubs < Base
46
- MSG = "Avoid stubbing Sidekiq enqueue methods (`perform_async`/`perform_at`/`perform_in`). " \
47
- "Stubs bypass `Sidekiq.strict_args!` validation and can hide symbol-keyed hash bugs " \
48
- "(see COR-1923). Use `have_enqueued_sidekiq_job` or `change(Job.jobs, :size)` instead. " \
49
- "Use `.and_call_original` only as an escape hatch.".freeze
57
+ MSG = "Avoid stubbing job/mailer enqueue methods (`perform_async`/`perform_at`/`perform_in`, " \
58
+ "or `Mailer.later`). Stubs bypass `Sidekiq.strict_args!` validation and can hide " \
59
+ "symbol-keyed hash bugs (see COR-1923). Use `have_enqueued_sidekiq_job` or " \
60
+ "`change(Job.jobs, :size)` instead. Use `.and_call_original` only as an escape hatch.".freeze
61
+
62
+ SUBJECT_WRAPPERS = [:allow, :expect, :allow_any_instance_of, :expect_any_instance_of].freeze
50
63
 
51
64
  def_node_matcher :perform_method_stub?, <<~PATTERN
52
65
  (send nil? {:receive :have_received} (sym {:perform_async :perform_at :perform_in}))
53
66
  PATTERN
54
67
 
68
+ def_node_matcher :later_method_stub?, <<~PATTERN
69
+ (send nil? {:receive :have_received} (sym :later))
70
+ PATTERN
71
+
55
72
  def on_send(node)
56
- return unless perform_method_stub?(node)
73
+ return unless perform_method_stub?(node) || mailer_later_stub?(node)
57
74
  return if chained_with_call_original?(node)
58
75
 
59
76
  add_offense(node)
@@ -61,6 +78,26 @@ module RuboCop
61
78
 
62
79
  private
63
80
 
81
+ def mailer_later_stub?(node)
82
+ return false unless later_method_stub?(node)
83
+
84
+ subject = stubbed_subject(node)
85
+ subject&.const_type? && subject.const_name.to_s.end_with?("Mailer")
86
+ end
87
+
88
+ def stubbed_subject(node)
89
+ current = node
90
+ current = current.parent while current.parent&.send_type? && current.parent.receiver == current
91
+
92
+ matcher = current.parent
93
+ return unless matcher&.send_type?
94
+
95
+ wrapper = matcher.receiver
96
+ return unless wrapper&.send_type? && SUBJECT_WRAPPERS.include?(wrapper.method_name)
97
+
98
+ wrapper.first_argument
99
+ end
100
+
64
101
  def chained_with_call_original?(node)
65
102
  current = node
66
103
  loop do
@@ -1,5 +1,5 @@
1
1
  module Sequra
2
2
  module Style
3
- VERSION = "1.18.0"
3
+ VERSION = "1.19.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequra-style
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sequra engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-28 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop