sequra-style 1.13.0 → 1.14.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: 638d231a616190beeb2b468652fe080b7b1e7e5b40f71aec06009ec44d14b17c
4
- data.tar.gz: 27d7a897202882a5535cefb4af8de185132f7e05f2aa00be2735aff1df4848ed
3
+ metadata.gz: 3d6244fe18ea0a416aa78a3f808972f72e8951a5b1f4b8fe6d8bde547519a062
4
+ data.tar.gz: c6a4e0354de8afabe25ab07ab269109e07b572ef76f746ed8e194b2063e35c08
5
5
  SHA512:
6
- metadata.gz: ea38ecc3ed4116d5c854683a74c3ff670a540c70a0b5ecbd609a4207a02874c469cfabefca9ec45bdc5d15da56beb42ec1aef5d8279dfdacf8f70f8413ae2241
7
- data.tar.gz: cd260081df82a6e2fcb7b23d5338872f1571a4c9f380fd826b6647a57756def2e3996e7be3ea9ef84c32529d3411054687da1f56fac7e959fd322d51d1f968f7
6
+ metadata.gz: 48b797b66b13747ec048dd26121a5c8ca225e694e13527fdad286e825b42dd29d0177cf9cdf0f4bd7f10b674538b318e95a4538b2122a9dd5d0a3a3196c2b0ea
7
+ data.tar.gz: 80f63fb2c00f3f295fec302fb748aaa30de666ec002aa666a5bd2f10e98197f283f70f0d5400b6212ac4d926b5eac8e04a554f0a012cca0c1c5c047a0a531947
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.14.0](https://github.com/sequra/sequra-style/compare/v1.13.0...v1.14.0) (2026-05-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * [COR-1992] Add Sequra/NoSidekiqPerformStubs cop ([#70](https://github.com/sequra/sequra-style/issues/70)) ([1a89ca4](https://github.com/sequra/sequra-style/commit/1a89ca4a9bcf2f12837f4d54d4aa5f4e7d54c899))
9
+
3
10
  ## [1.13.0](https://github.com/sequra/sequra-style/compare/v1.12.1...v1.13.0) (2026-04-24)
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.13.0)
4
+ sequra-style (1.14.0)
5
5
  rubocop (~> 1.75)
6
6
  rubocop-performance (~> 1.25)
7
7
  rubocop-rails (~> 2.31)
data/default.yml CHANGED
@@ -832,3 +832,16 @@ Sequra/AsyncJobPattern:
832
832
  - "packs/*/app/workers/**/*.rb"
833
833
  Exclude:
834
834
  - "**/application_job.rb"
835
+
836
+ # Forbid stubbing/spying on Sidekiq enqueue methods in specs. Stubs bypass
837
+ # `Sidekiq.strict_args!` validation, which has caused silent production
838
+ # failures (COR-1923). Use `have_enqueued_sidekiq_job` or
839
+ # `change(Job.jobs, :size)` instead. `.and_call_original` is exempt.
840
+ # Ships disabled so consumer repos opt in on their own timeline (paired with
841
+ # `.rubocop_todo.yml` to grandfather existing offenses).
842
+ Sequra/NoSidekiqPerformStubs:
843
+ Enabled: false
844
+ Include:
845
+ - "**/*_spec.rb"
846
+ - "**/spec/support/**/*.rb"
847
+ - "**/spec/shared_examples/**/*.rb"
@@ -0,0 +1,77 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Sequra
4
+ # Detects RSpec stubs and spy assertions on Sidekiq enqueue methods
5
+ # (`perform_async`, `perform_at`, `perform_in`).
6
+ #
7
+ # Stubbing these methods bypasses `Sidekiq.strict_args!` validation,
8
+ # which lets symbol-keyed hashes (and other non-JSON-native types)
9
+ # slip through unchecked. The same code would raise `ArgumentError`
10
+ # in production at enqueue time, and the bug only surfaces when the
11
+ # job actually runs — by which point retries, timing windows, and
12
+ # rescue-swallowers can mask the failure. This pattern caused a
13
+ # silent push-notification drop in COR-1923.
14
+ #
15
+ # Use `have_enqueued_sidekiq_job` (or `change(SomeJob.jobs, :size)`)
16
+ # instead. Both go through Sidekiq's client middleware chain, so
17
+ # `strict_args!` validates the arguments as it would in production.
18
+ #
19
+ # `.and_call_original` is exempt: the stub spies on the call but
20
+ # then runs the real `perform_*`, which still exercises the
21
+ # serialization contract.
22
+ #
23
+ # @example
24
+ # # bad - stub bypasses strict_args! validation
25
+ # allow(SomeJob).to receive(:perform_async)
26
+ #
27
+ # # bad - even with arg matchers, the real enqueue is suppressed
28
+ # expect(SomeJob).to receive(:perform_at).with(time, data)
29
+ #
30
+ # # bad - negative assertion still bypasses the contract
31
+ # expect(SomeJob).not_to receive(:perform_async)
32
+ #
33
+ # # bad - spy verification on a stubbed enqueue
34
+ # expect(SomeJob).to have_received(:perform_async).with(id)
35
+ #
36
+ # # good - exercises strict_args! through Sidekiq's middleware
37
+ # expect(SomeJob).to have_enqueued_sidekiq_job(id, name: "value")
38
+ #
39
+ # # good - state-based assertion, also exercises the middleware
40
+ # expect { subject }.to change(SomeJob.jobs, :size).by(1)
41
+ #
42
+ # # good - escape hatch: spy AND run the real method
43
+ # expect(SomeJob).to receive(:perform_async).and_call_original
44
+ #
45
+ 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
50
+
51
+ def_node_matcher :perform_method_stub?, <<~PATTERN
52
+ (send nil? {:receive :have_received} (sym {:perform_async :perform_at :perform_in}))
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return unless perform_method_stub?(node)
57
+ return if chained_with_call_original?(node)
58
+
59
+ add_offense(node)
60
+ end
61
+
62
+ private
63
+
64
+ def chained_with_call_original?(node)
65
+ current = node
66
+ loop do
67
+ parent = current.parent
68
+ return false unless parent&.send_type? && parent.receiver == current
69
+ return true if parent.method_name == :and_call_original
70
+
71
+ current = parent
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  module Sequra
2
2
  module Style
3
- VERSION = "1.13.0"
3
+ VERSION = "1.14.0"
4
4
  end
5
5
  end
data/lib/sequra_style.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  require "rubocop"
2
2
  require_relative "rubocop/cop/sequra/async_job_pattern"
3
+ require_relative "rubocop/cop/sequra/no_sidekiq_perform_stubs"
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.13.0
4
+ version: 1.14.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-04-24 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -135,6 +135,7 @@ files:
135
135
  - docs/decisions/index.md
136
136
  - docs/decisions/template.md
137
137
  - lib/rubocop/cop/sequra/async_job_pattern.rb
138
+ - lib/rubocop/cop/sequra/no_sidekiq_perform_stubs.rb
138
139
  - lib/sequra/style.rb
139
140
  - lib/sequra/style/version.rb
140
141
  - lib/sequra_style.rb