rspec-goodies 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 688595f20fe784a45182edf3ebdcb7f19a411ad3dd975259b0022631d925f788
4
+ data.tar.gz: f9a6bd4961893ceab3d91de8592d6ce8ca883d2c68a1706212144e0d74972dcd
5
+ SHA512:
6
+ metadata.gz: 430633d6935b63b479bc46e9fc1e02dcf65f73ddd5ab8aa38db6efe3adc4c0c85681c2ad0eccc00fd720eefaf958c0566b19e7ae7dc9f2132c223d940a33e5bc
7
+ data.tar.gz: 14841a867c03923ec9af50fa0eda9d02b5d88f9c64ba7dc9783ec4be283ab3fc4e865bdaf3c04d52dc11ee1202b23c6c8bf36277355944978915bceac8131191
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rspec-goodies.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,218 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rspec-goodies (0.0.1)
5
+ activesupport
6
+ byebug
7
+ rails
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actioncable (7.1.2)
13
+ actionpack (= 7.1.2)
14
+ activesupport (= 7.1.2)
15
+ nio4r (~> 2.0)
16
+ websocket-driver (>= 0.6.1)
17
+ zeitwerk (~> 2.6)
18
+ actionmailbox (7.1.2)
19
+ actionpack (= 7.1.2)
20
+ activejob (= 7.1.2)
21
+ activerecord (= 7.1.2)
22
+ activestorage (= 7.1.2)
23
+ activesupport (= 7.1.2)
24
+ mail (>= 2.7.1)
25
+ net-imap
26
+ net-pop
27
+ net-smtp
28
+ actionmailer (7.1.2)
29
+ actionpack (= 7.1.2)
30
+ actionview (= 7.1.2)
31
+ activejob (= 7.1.2)
32
+ activesupport (= 7.1.2)
33
+ mail (~> 2.5, >= 2.5.4)
34
+ net-imap
35
+ net-pop
36
+ net-smtp
37
+ rails-dom-testing (~> 2.2)
38
+ actionpack (7.1.2)
39
+ actionview (= 7.1.2)
40
+ activesupport (= 7.1.2)
41
+ nokogiri (>= 1.8.5)
42
+ racc
43
+ rack (>= 2.2.4)
44
+ rack-session (>= 1.0.1)
45
+ rack-test (>= 0.6.3)
46
+ rails-dom-testing (~> 2.2)
47
+ rails-html-sanitizer (~> 1.6)
48
+ actiontext (7.1.2)
49
+ actionpack (= 7.1.2)
50
+ activerecord (= 7.1.2)
51
+ activestorage (= 7.1.2)
52
+ activesupport (= 7.1.2)
53
+ globalid (>= 0.6.0)
54
+ nokogiri (>= 1.8.5)
55
+ actionview (7.1.2)
56
+ activesupport (= 7.1.2)
57
+ builder (~> 3.1)
58
+ erubi (~> 1.11)
59
+ rails-dom-testing (~> 2.2)
60
+ rails-html-sanitizer (~> 1.6)
61
+ activejob (7.1.2)
62
+ activesupport (= 7.1.2)
63
+ globalid (>= 0.3.6)
64
+ activemodel (7.1.2)
65
+ activesupport (= 7.1.2)
66
+ activerecord (7.1.2)
67
+ activemodel (= 7.1.2)
68
+ activesupport (= 7.1.2)
69
+ timeout (>= 0.4.0)
70
+ activestorage (7.1.2)
71
+ actionpack (= 7.1.2)
72
+ activejob (= 7.1.2)
73
+ activerecord (= 7.1.2)
74
+ activesupport (= 7.1.2)
75
+ marcel (~> 1.0)
76
+ activesupport (7.1.2)
77
+ base64
78
+ bigdecimal
79
+ concurrent-ruby (~> 1.0, >= 1.0.2)
80
+ connection_pool (>= 2.2.5)
81
+ drb
82
+ i18n (>= 1.6, < 2)
83
+ minitest (>= 5.1)
84
+ mutex_m
85
+ tzinfo (~> 2.0)
86
+ base64 (0.1.1)
87
+ bigdecimal (3.1.4)
88
+ builder (3.2.4)
89
+ byebug (11.1.3)
90
+ concurrent-ruby (1.2.2)
91
+ connection_pool (2.4.1)
92
+ crass (1.0.6)
93
+ date (3.3.4)
94
+ diff-lcs (1.5.0)
95
+ drb (2.1.1)
96
+ ruby2_keywords
97
+ erubi (1.12.0)
98
+ globalid (1.2.1)
99
+ activesupport (>= 6.1)
100
+ i18n (1.14.1)
101
+ concurrent-ruby (~> 1.0)
102
+ io-console (0.7.1)
103
+ irb (1.11.0)
104
+ rdoc
105
+ reline (>= 0.3.8)
106
+ loofah (2.22.0)
107
+ crass (~> 1.0.2)
108
+ nokogiri (>= 1.12.0)
109
+ mail (2.8.1)
110
+ mini_mime (>= 0.1.1)
111
+ net-imap
112
+ net-pop
113
+ net-smtp
114
+ marcel (1.0.2)
115
+ mini_mime (1.1.5)
116
+ minitest (5.20.0)
117
+ mutex_m (0.1.2)
118
+ net-imap (0.4.9)
119
+ date
120
+ net-protocol
121
+ net-pop (0.1.2)
122
+ net-protocol
123
+ net-protocol (0.2.2)
124
+ timeout
125
+ net-smtp (0.4.0)
126
+ net-protocol
127
+ nio4r (2.7.0)
128
+ nokogiri (1.15.5-arm64-darwin)
129
+ racc (~> 1.4)
130
+ psych (5.1.2)
131
+ stringio
132
+ racc (1.7.3)
133
+ rack (3.0.8)
134
+ rack-session (2.0.0)
135
+ rack (>= 3.0.0)
136
+ rack-test (2.1.0)
137
+ rack (>= 1.3)
138
+ rackup (2.1.0)
139
+ rack (>= 3)
140
+ webrick (~> 1.8)
141
+ rails (7.1.2)
142
+ actioncable (= 7.1.2)
143
+ actionmailbox (= 7.1.2)
144
+ actionmailer (= 7.1.2)
145
+ actionpack (= 7.1.2)
146
+ actiontext (= 7.1.2)
147
+ actionview (= 7.1.2)
148
+ activejob (= 7.1.2)
149
+ activemodel (= 7.1.2)
150
+ activerecord (= 7.1.2)
151
+ activestorage (= 7.1.2)
152
+ activesupport (= 7.1.2)
153
+ bundler (>= 1.15.0)
154
+ railties (= 7.1.2)
155
+ rails-dom-testing (2.2.0)
156
+ activesupport (>= 5.0.0)
157
+ minitest
158
+ nokogiri (>= 1.6)
159
+ rails-html-sanitizer (1.6.0)
160
+ loofah (~> 2.21)
161
+ nokogiri (~> 1.14)
162
+ railties (7.1.2)
163
+ actionpack (= 7.1.2)
164
+ activesupport (= 7.1.2)
165
+ irb
166
+ rackup (>= 1.0.0)
167
+ rake (>= 12.2)
168
+ thor (~> 1.0, >= 1.2.2)
169
+ zeitwerk (~> 2.6)
170
+ rake (13.1.0)
171
+ rdoc (6.6.2)
172
+ psych (>= 4.0.0)
173
+ redis-client (0.19.1)
174
+ connection_pool
175
+ reline (0.4.1)
176
+ io-console (~> 0.5)
177
+ rspec (3.12.0)
178
+ rspec-core (~> 3.12.0)
179
+ rspec-expectations (~> 3.12.0)
180
+ rspec-mocks (~> 3.12.0)
181
+ rspec-core (3.12.2)
182
+ rspec-support (~> 3.12.0)
183
+ rspec-expectations (3.12.3)
184
+ diff-lcs (>= 1.2.0, < 2.0)
185
+ rspec-support (~> 3.12.0)
186
+ rspec-mocks (3.12.6)
187
+ diff-lcs (>= 1.2.0, < 2.0)
188
+ rspec-support (~> 3.12.0)
189
+ rspec-support (3.12.1)
190
+ ruby2_keywords (0.0.5)
191
+ sidekiq (7.2.0)
192
+ concurrent-ruby (< 2)
193
+ connection_pool (>= 2.3.0)
194
+ rack (>= 2.2.4)
195
+ redis-client (>= 0.14.0)
196
+ stringio (3.1.0)
197
+ thor (1.3.0)
198
+ timecop (0.9.8)
199
+ timeout (0.4.1)
200
+ tzinfo (2.0.6)
201
+ concurrent-ruby (~> 1.0)
202
+ webrick (1.8.1)
203
+ websocket-driver (0.7.6)
204
+ websocket-extensions (>= 0.1.0)
205
+ websocket-extensions (0.1.5)
206
+ zeitwerk (2.6.12)
207
+
208
+ PLATFORMS
209
+ arm64-darwin-21
210
+
211
+ DEPENDENCIES
212
+ rspec
213
+ rspec-goodies!
214
+ sidekiq
215
+ timecop
216
+
217
+ BUNDLED WITH
218
+ 2.4.20
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 James Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # rspec-goodies
2
+
3
+ Provides RSpec helpers and matchers for stubs, Sidekiq, and more.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "rspec-goodies"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```shell
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```shell
22
+ gem install rspec-goodies
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Within `spec_helper.rb` or `rails_helper.rb`
28
+
29
+ ```ruby
30
+ require "rspec-goodies"
31
+ ```
32
+
33
+ ## Development
34
+
35
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
+
37
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/axsuul/rspec-goodies.
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,156 @@
1
+ require "active_support"
2
+ require "active_support/core_ext/hash"
3
+
4
+ module RSpec
5
+ module Goodies
6
+ module Helpers
7
+ module Sidekiq
8
+ # Helper to briefly enable unique jobs functionality Only use this if you're explicitly testing unique job
9
+ # behavior otherwise it is very unpredictable and it's best to disable it for normal tests
10
+ def within_sidekiq_unique
11
+ ::Sidekiq::Enterprise.unique!
12
+
13
+ yield
14
+ ensure
15
+ disable_sidekiq_unique!
16
+ end
17
+
18
+ def disable_sidekiq_unique!
19
+ ::Sidekiq.configure_server do |config|
20
+ config.server_middleware do |chain|
21
+ chain.remove(::Sidekiq::Enterprise::Unique::Server)
22
+ end
23
+ end
24
+
25
+ ::Sidekiq.configure_client do |config|
26
+ config.client_middleware do |chain|
27
+ chain.remove(::Sidekiq::Enterprise::Unique::Client)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Simulate job being performed within the entire middleware stack
33
+ def perform_sidekiq_job(worker_class, *args)
34
+ worker_class.perform_async(*args)
35
+
36
+ actual_worker_class =
37
+ if worker_class.is_a?(::Sidekiq::Worker::Setter)
38
+ worker_class.instance_variable_get("@klass")
39
+ else
40
+ worker_class
41
+ end
42
+
43
+ actual_worker_class.process_job(actual_worker_class.jobs.last)
44
+ end
45
+
46
+ def process_sidekiq_payloads(payloads)
47
+ payloads.each do |payload|
48
+ payload_hash = ::Sidekiq.load_json(payload)
49
+ worker_class = ::Sidekiq::Testing.constantize(payload_hash["class"])
50
+
51
+ worker_class.process_job(payload_hash)
52
+ end
53
+ end
54
+
55
+ def add_enqueued_sidekiq_job(worker_class:, args:, metadata: {}, jid: SecureRandom.hex, queue: :default)
56
+ payload = ::Sidekiq.dump_json(
57
+ metadata.merge(
58
+ "jid" => jid,
59
+ "class" => worker_class.to_s,
60
+ "args" => args,
61
+ "queue" => queue.to_s,
62
+ "enqueued_at" => Time.current.to_f,
63
+ ),
64
+ )
65
+
66
+ ::Sidekiq.redis do |redis|
67
+ redis.sadd("queues", queue.to_s)
68
+ redis.lpush("queue:#{queue}", payload)
69
+ end
70
+
71
+ payload
72
+ end
73
+
74
+ # Simulate job being added to retry queue (based off Sidekiq source code)
75
+ def add_retry_sidekiq_job(
76
+ worker_class:,
77
+ args:,
78
+ metadata: {},
79
+ jid: SecureRandom.hex,
80
+ retry_count: 2,
81
+ retry_at: 1.hour.from_now
82
+ )
83
+ payload = ::Sidekiq.dump_json(
84
+ metadata.merge(
85
+ "jid" => jid,
86
+ "class" => worker_class.to_s,
87
+ "args" => args,
88
+ "queue" => "default",
89
+ "failed_at" => Time.now.to_f,
90
+ "retry_count" => retry_count,
91
+ "error_backtrace" => ["line1", "line2"],
92
+ ),
93
+ )
94
+
95
+ ::Sidekiq.redis do |redis|
96
+ redis.zadd("retry", retry_at.to_f.to_s, payload)
97
+ end
98
+
99
+ payload
100
+ end
101
+
102
+ def add_scheduled_sidekiq_job(worker_class:, args:, metadata: {}, jid: SecureRandom.hex)
103
+ payload = ::Sidekiq.dump_json(
104
+ metadata.merge(
105
+ "jid" => jid,
106
+ "class" => worker_class.to_s,
107
+ "args" => args,
108
+ ),
109
+ )
110
+ score = Time.now.to_f
111
+
112
+ ::Sidekiq.redis do |redis|
113
+ redis.zadd("schedule", score, payload)
114
+ end
115
+
116
+ payload
117
+ end
118
+
119
+ # Simulate job being worked on a worker so that's available via the Sidekiq::Workers.new API (based off Sidekiq
120
+ # source code)
121
+ def add_in_progress_sidekiq_job(worker_class:, args:, jid: SecureRandom.hex)
122
+ @sidekiq_job_in_progress_thread_id ||= 1000
123
+ @sidekiq_job_in_progress_count ||= 0
124
+
125
+ process_id = "foo:#{SecureRandom.hex}"
126
+ job_data = ::Sidekiq.dump_json(
127
+ "queue" => "default",
128
+ "payload" => {
129
+ "jid" => jid,
130
+ "class" => worker_class.to_s,
131
+ "args" => args,
132
+ },
133
+ "run_at" => Time.current.to_i,
134
+ )
135
+ process_data = ::Sidekiq.dump_json(
136
+ "hostname" => "foo",
137
+ "started_at" => Time.now.to_f,
138
+ "queues" => ["default"],
139
+ )
140
+
141
+ ::Sidekiq.redis do |redis|
142
+ redis.incr("busy")
143
+ redis.sadd("processes", process_id)
144
+ redis.hmset(
145
+ process_id, "info",
146
+ process_data, "at",
147
+ Time.current.to_f, "busy",
148
+ @sidekiq_job_in_progress_count += 1,
149
+ )
150
+ redis.hmset("#{process_id}:work", @sidekiq_job_in_progress_thread_id += 1, job_data)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,76 @@
1
+ module RSpec
2
+ module Goodies
3
+ module Helpers
4
+ module Stubs
5
+ def stub_service_as_spy(klass)
6
+ service_stub = class_spy(klass)
7
+ stub_const(klass.to_s, service_stub)
8
+
9
+ service_stub
10
+ end
11
+
12
+ def stub_rails_logger_as_spy
13
+ instance_spy("ActiveSupport::Logger").tap do |stub|
14
+ allow(::Rails).to receive(:logger).and_return(stub)
15
+ end
16
+ end
17
+
18
+ def stub_class_const(klass, const_string, value)
19
+ raise ArgumentError, "a Class or Module must be passed in" if !klass.is_a?(Class) && !klass.is_a?(Module)
20
+
21
+ # Check that constant actually exists. Also by calling klass here we ensure it's loaded before stubbing
22
+ # constant
23
+ unless klass.const_defined?(const_string)
24
+ raise Exception, "Tried to stub #{klass}::#{const_string} but it doesn't exist!"
25
+ end
26
+
27
+ stub_const("#{klass.name}::#{const_string}", value)
28
+ end
29
+
30
+ # Stubs existing constant with resulting hash deep merged with existing hash and hash passed in
31
+ def stub_class_const_and_merge(klass, const_string, hash)
32
+ raise ArgumentError, "must pass in hash" unless hash.is_a?(Hash)
33
+
34
+ existing_hash = klass.const_get(const_string)
35
+
36
+ stub_class_const(klass, const_string, existing_hash.deep_merge(hash))
37
+ end
38
+
39
+ # Stub environment variable so that it doesn't leak out of tests
40
+ def stub_env(name, value)
41
+ allow(ENV).to receive(:[]).with(name).and_return(value)
42
+ end
43
+
44
+ # Only works with nested credentials for now
45
+ def stub_rails_credentials(stubbed_config)
46
+ raise NotImplementedError, "Rails not found" unless Object.const_defined?(:Rails)
47
+
48
+ credentials = ::Rails.application.credentials
49
+ credentials_config = credentials.config
50
+ stubbed_credentials_config = credentials_config
51
+
52
+ stubbed_config.each do |key, key_stubbed_config|
53
+ key = key.to_sym
54
+
55
+ case key_stubbed_config
56
+ when Hash
57
+ key_stubbed_config.each do |child_key, _|
58
+ if !credentials_config.key?(key) || !credentials_config[key].key?(child_key)
59
+ raise ArgumentError, "Tried to stub Rails credential #{key}: #{child_key} but it doesn't exist!"
60
+ end
61
+ end
62
+
63
+ # Merge in existing credentials so we don't break accessing other credentials
64
+ stubbed_credentials_config[key].merge!(key_stubbed_config)
65
+ when String
66
+ # Not nested so set it directly
67
+ stubbed_credentials_config[key] = key_stubbed_config
68
+ end
69
+ end
70
+
71
+ allow(::Rails.application).to receive(:credentials).and_return(OpenStruct.new(stubbed_credentials_config))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,50 @@
1
+ # Used to match if array includes elements and when order matters
2
+ RSpec::Matchers.define :include_in_order do |*args|
3
+ match do |collection|
4
+ expect_to_match = lambda do |value, matcher|
5
+ # If composable matcher (e.g. a_string_matching(...) then we need to use a different expectation)
6
+ if matcher.class.name.match?(/Matcher/)
7
+ expect(value).to match(matcher)
8
+ else
9
+ expect(value).to eq(matcher)
10
+ end
11
+ end
12
+
13
+ is_matched_in_order = false
14
+
15
+ # Go through collection and try to find the first match
16
+ collection.each_with_index do |element, collection_index|
17
+ initial_matcher = args.first
18
+
19
+ expect_to_match.call(element, initial_matcher)
20
+
21
+ # If reaches here, that means we found the first match so let's see if the remaining also match in order
22
+ is_matched_in_order = args[1..-1].each_with_index.all? do |pending_matcher, pending_matcher_index|
23
+ expect_to_match.call(collection[collection_index + pending_matcher_index + 1], pending_matcher)
24
+
25
+ true
26
+ rescue RSpec::Expectations::ExpectationNotMetError
27
+ false
28
+ end
29
+
30
+ # No need to search anymore once we found it
31
+ break if is_matched_in_order
32
+ rescue RSpec::Expectations::ExpectationNotMetError
33
+ # Keep trying to find the first match
34
+ end
35
+
36
+ is_matched_in_order
37
+ end
38
+ end
39
+
40
+ RSpec::Matchers.alias_matcher :a_collection_including_in_order, :include_in_order
41
+
42
+ # So we can do:
43
+ #
44
+ # expect(collection).to not_any(...)
45
+ #
46
+ # since we can't do:
47
+ #
48
+ # expect(collection).not_to all(...)
49
+ #
50
+ RSpec::Matchers.define_negated_matcher :not_any, :include
@@ -0,0 +1,24 @@
1
+ RSpec::Matchers.define :match_timestamp do |expected, decimal_places = nil|
2
+ normalize = lambda do |value|
3
+ value = DateTime.parse(value) if value.is_a?(String)
4
+
5
+ # Compare floats since there can be sub-seconds
6
+ normalized = value.to_f
7
+
8
+ # Can be limited to decimals places (e.g. 0.002 can match 0.0019 if "3" is provided)
9
+ normalized = normalized.round(decimal_places) if decimal_places
10
+
11
+ normalized
12
+ end
13
+
14
+ match do |actual|
15
+ normalize.call(actual) == normalize.call(expected)
16
+ end
17
+
18
+ failure_message do |actual|
19
+ <<~MESSAGE
20
+ expected that #{actual} (#{normalize.call(actual)}) would match timestamp #{expected}
21
+ (#{normalize.call(expected)})"
22
+ MESSAGE
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ # Used to match if hash doesn't have all keys
2
+ RSpec::Matchers.define :not_have_keys do |*keys|
3
+ match do |hash|
4
+ keys.all? { |k| !hash.key?(k) }
5
+ end
6
+ end
7
+
8
+ RSpec::Matchers.alias_matcher :a_hash_without_keys, :not_have_keys
@@ -0,0 +1,214 @@
1
+ require "rspec/matchers"
2
+ require "sidekiq/testing"
3
+
4
+ class SidekiqJobsEnqueuedMatcher
5
+ include RSpec::Matchers
6
+
7
+ attr_reader :new_jobs, :new_jobs_matching_properties
8
+
9
+ def initialize(actual, worker_class, expected_size = nil, expected_properties = {})
10
+ @actual = actual
11
+ @worker_class = worker_class
12
+
13
+ # Size is optional to be passed in
14
+ if expected_size.is_a?(Hash)
15
+ @expected_properties = expected_size
16
+ @expected_size = nil
17
+ else
18
+ @expected_properties = expected_properties
19
+ @expected_size = expected_size
20
+ end
21
+
22
+ # Normalize
23
+ @expected_properties.stringify_keys!
24
+ end
25
+
26
+ def matches?
27
+ # Get jobs before so that we only perform new jobs
28
+ jobs_before = @worker_class.jobs.clone
29
+
30
+ # Calls the actual block of the matcher
31
+ @actual.call
32
+
33
+ jobs_after = @worker_class.jobs
34
+ @new_jobs = jobs_after - jobs_before
35
+
36
+ @new_jobs_matching_properties = @new_jobs.select do |job|
37
+ matched_count = 0
38
+
39
+ @expected_properties.each do |key, value|
40
+ case key
41
+ when "args"
42
+ # Coerce to an array unless it's a matcher
43
+ value = Array(value) unless value.respond_to?(:base_matcher)
44
+
45
+ expect(Array(job[key])).to match(value)
46
+ when "at"
47
+ if (scheduled_at = job[key])
48
+ # It's a float in job
49
+ expect(scheduled_at).to eq(value.to_f)
50
+
51
+ # Otherwise if nil, then we are looking for jobs that aren't scheduled
52
+ else
53
+ expect(job.key?("at")).to eq false
54
+ end
55
+ when "bid"
56
+ expect(job[key]).to match(value)
57
+ when "queue", "unique_for", "unique_until"
58
+ expect(job[key].to_s).to eq(value.to_s)
59
+ when "metadata"
60
+ # Even though we call this metadata, it's actually just keys in the job hash. We iterate through each key so
61
+ # that it also works on checking for nil values
62
+ value.each do |metadata_key, metadata_value|
63
+ expect(job[metadata_key]).to eq metadata_value
64
+ end
65
+ end
66
+
67
+ matched_count += 1
68
+ rescue RSpec::Expectations::ExpectationNotMetError
69
+ # Doesn't contribute to matched_count if any expectations fail
70
+ end
71
+
72
+ matched_count == @expected_properties.count
73
+ end
74
+
75
+ # Check expected number of new jobs enqueued to match size if specified
76
+ if @expected_size
77
+ @new_jobs_matching_properties.size == @expected_size
78
+ else
79
+ @new_jobs_matching_properties.any?
80
+ end
81
+ end
82
+
83
+ def new_jobs_sanitized
84
+ (@new_jobs || []).map { |j| j.except("jid", "backtrace", "retry", "created_at", "enqueued_at") }
85
+ end
86
+ end
87
+
88
+ RSpec::Matchers.define :have_sidekiq_jobs_enqueued do |*args|
89
+ match do |actual|
90
+ @matcher = SidekiqJobsEnqueuedMatcher.new(actual, *args)
91
+
92
+ @matcher.matches?
93
+ end
94
+
95
+ failure_message do |_|
96
+ "expected Sidekiq jobs to be enqueued with #{args.join(', ')} but instead found:\n\n#{@matcher.new_jobs_sanitized.pretty_inspect}"
97
+ end
98
+
99
+ def supports_block_expectations?
100
+ true
101
+ end
102
+ end
103
+
104
+ RSpec::Matchers.alias_matcher :have_sidekiq_job_enqueued, :have_sidekiq_jobs_enqueued
105
+ RSpec::Matchers.define_negated_matcher :not_have_sidekiq_jobs_enqueued, :have_sidekiq_jobs_enqueued
106
+
107
+ RSpec::Matchers.define :have_sidekiq_jobs_enqueued_and_performed do |*args|
108
+ match do |actual|
109
+ # Add the first arguments to the end since we are processing the chain from
110
+ # last to first
111
+ @worker_chain ||= []
112
+ @worker_chain << args
113
+
114
+ # Alias
115
+ perform_jobs = actual
116
+
117
+ while @worker_chain.any?
118
+ args = @worker_chain.pop
119
+ worker_class = args.first
120
+
121
+ @matcher = SidekiqJobsEnqueuedMatcher.new(perform_jobs, *args)
122
+
123
+ expect(@matcher.matches?).to eq true
124
+
125
+ new_jobs_matching_properties = @matcher.new_jobs_matching_properties
126
+
127
+ perform_jobs = lambda do
128
+ new_jobs_matching_properties.each do |job|
129
+ worker_class = job["class"]
130
+
131
+ # Remove from queue after performing it
132
+ Sidekiq::Queues.delete_for(job["jid"], job["queue"], worker_class)
133
+
134
+ worker_class.constantize.process_job(job)
135
+ end
136
+ end
137
+ end
138
+
139
+ # Make sure we call perform jobs one last time if there's no more in chain
140
+ perform_jobs.call
141
+ end
142
+
143
+ chain :thereafter do |*next_args|
144
+ @worker_chain ||= []
145
+ @worker_chain.prepend(next_args)
146
+ end
147
+
148
+ failure_message do |_|
149
+ "expected Sidekiq jobs to be enqueued and performed with #{args.join(', ')} but instead found:\n\n#{@matcher.new_jobs_sanitized.pretty_inspect}"
150
+ end
151
+
152
+ def supports_block_expectations?
153
+ true
154
+ end
155
+ end
156
+
157
+ RSpec::Matchers.define :have_sidekiq_batch_with_callback_triggered do |event, callback_class|
158
+ match do |actual|
159
+ received_callbacks = Hash.new do |hash, key|
160
+ hash[key] = {
161
+ death: [],
162
+ complete: [],
163
+ success: [],
164
+ }
165
+ end
166
+
167
+ sidekiq_batch_stub = ::Sidekiq::Batch.new.tap do |stub|
168
+ # This can be called multiple times so track each time
169
+ allow(stub).to receive(:on) do |actual_event, actual_callback_class, actual_callback_options|
170
+ received_callbacks[actual_callback_class.name][actual_event].push(actual_callback_options)
171
+ end
172
+
173
+ allow(::Sidekiq::Batch).to receive(:new).and_return(stub)
174
+ end
175
+
176
+ # Ensure stuff happens within batch
177
+ allow(sidekiq_batch_stub).to receive(:jobs).and_call_original
178
+
179
+ actual.call
180
+
181
+ # Only care about those received for event and callback
182
+ relevant_received_callbacks = received_callbacks[callback_class.to_s][event]
183
+
184
+ expect(relevant_received_callbacks).to be_any
185
+
186
+ # Once block is complete, simulate Sidekiq batch callback is called by manually calling it ourselves since it
187
+ # doesn't automatically get called in tests
188
+ ::Sidekiq::Batch.new.tap do |batch|
189
+ # Ensure created in redis so that we can access it
190
+ batch.jobs {}
191
+
192
+ # Simulate that callback is called for each time callback options were received
193
+ relevant_received_callbacks.each do |callback_options|
194
+ # Also simulate that callback options are converted to JSON and back to hash which is what happens in production
195
+ callback_class.new.send(
196
+ :"on_#{event}",
197
+ ::Sidekiq::Batch::Status.new(batch.bid),
198
+ JSON.parse(callback_options.to_json),
199
+ )
200
+ end
201
+ end
202
+ end
203
+
204
+ failure_message do |_|
205
+ "expected jobs to perform within Sidekiq batch with #{callback_class} callback to be triggered by #{event}"
206
+ end
207
+
208
+ def supports_block_expectations?
209
+ true
210
+ end
211
+ end
212
+
213
+ RSpec::Matchers.define_negated_matcher :not_have_sidekiq_batch_with_callback_triggered,
214
+ :have_sidekiq_batch_with_callback_triggered
@@ -0,0 +1,16 @@
1
+ RSpec::Matchers.define_negated_matcher :a_string_not_matching, :a_string_matching
2
+
3
+ RSpec::Matchers.define :match_url do |matched_url|
4
+ match do |url|
5
+ uri = Addressable::URI.parse(url)
6
+ matched_uri = Addressable::URI.parse(matched_url)
7
+
8
+ uri.host == matched_uri.host &&
9
+ uri.path == matched_uri.path &&
10
+ uri.query_values == matched_uri.query_values
11
+ end
12
+
13
+ failure_message do |url|
14
+ "expected the url: #{url} to match the url: #{matched_url}"
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "rspec/goodies/helpers/sidekiq"
5
+ require "rspec/goodies/helpers/stubs"
6
+ require "rspec/goodies/matchers/collection"
7
+ require "rspec/goodies/matchers/date_time"
8
+ require "rspec/goodies/matchers/hash"
9
+ require "rspec/goodies/matchers/sidekiq"
10
+ require "rspec/goodies/matchers/string"
11
+
12
+ RSpec.configure do |config|
13
+ config.include(RSpec::Goodies::Helpers::Sidekiq)
14
+ config.include(RSpec::Goodies::Helpers::Stubs)
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rspec-goodies"
5
+ spec.version = "0.0.1"
6
+ spec.authors = ["James Hu"]
7
+
8
+ spec.summary = "RSpec goodies full of helpers, matchers, and more"
9
+ spec.description = "RSpec goodies full of helpers, matchers, and more"
10
+ spec.homepage = "https://github.com/axsuul/rspec-goodies"
11
+ spec.license = "MIT"
12
+
13
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/axsuul/rspec-goodies"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject do |f|
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
+ end
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ # Uncomment to register a new dependency of your gem
29
+ spec.add_dependency "activesupport"
30
+ spec.add_dependency "byebug"
31
+ spec.add_dependency "rails"
32
+
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "sidekiq"
35
+ spec.add_development_dependency "timecop"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-goodies
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - James Hu
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-01-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sidekiq
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: RSpec goodies full of helpers, matchers, and more
98
+ email:
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - LICENSE.txt
106
+ - README.md
107
+ - Rakefile
108
+ - lib/rspec-goodies.rb
109
+ - lib/rspec/goodies/helpers/sidekiq.rb
110
+ - lib/rspec/goodies/helpers/stubs.rb
111
+ - lib/rspec/goodies/matchers/collection.rb
112
+ - lib/rspec/goodies/matchers/date_time.rb
113
+ - lib/rspec/goodies/matchers/hash.rb
114
+ - lib/rspec/goodies/matchers/sidekiq.rb
115
+ - lib/rspec/goodies/matchers/string.rb
116
+ - rspec-goodies.gemspec
117
+ homepage: https://github.com/axsuul/rspec-goodies
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ allowed_push_host: https://rubygems.org
122
+ homepage_uri: https://github.com/axsuul/rspec-goodies
123
+ source_code_uri: https://github.com/axsuul/rspec-goodies
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubygems_version: 3.1.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: RSpec goodies full of helpers, matchers, and more
143
+ test_files: []