pause 0.2.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,32 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pause/helper/timing'
2
4
 
3
5
  module Pause
4
6
  module Redis
5
-
6
7
  # This class encapsulates Redis operations used by Pause
7
8
  class Adapter
9
+ class << self
10
+ def redis
11
+ @redis ||= ::Redis.new(redis_connection_opts)
12
+ end
13
+
14
+ def redis_connection_opts
15
+ { host: Pause.config.redis_host,
16
+ port: Pause.config.redis_port,
17
+ db: Pause.config.redis_db }
18
+ end
19
+ end
8
20
 
9
21
  include Pause::Helper::Timing
10
22
  attr_accessor :resolution, :time_blocks_to_keep, :history
11
23
 
12
24
  def initialize(config)
13
- @resolution = config.resolution
25
+ @resolution = config.resolution
14
26
  @time_blocks_to_keep = config.history / @resolution
15
- @history = config.history
27
+ @history = config.history
28
+ end
29
+
30
+ # Override in subclasses to disable
31
+ def with_multi
32
+ redis.multi do |redis|
33
+ yield(redis) if block_given?
34
+ end
16
35
  end
17
36
 
18
37
  def increment(scope, identifier, timestamp, count = 1)
19
38
  k = tracked_key(scope, identifier)
20
- redis.multi do |redis|
39
+ with_multi do |redis|
21
40
  redis.zincrby k, count, period_marker(resolution, timestamp)
22
41
  redis.expire k, history
23
42
  end
24
43
 
25
- if redis.zcard(k) > time_blocks_to_keep
26
- list = extract_set_elements(k)
27
- to_remove = list.slice(0, (list.size - time_blocks_to_keep))
28
- redis.zrem(k, to_remove.map(&:ts))
29
- end
44
+ truncate_set_for(k)
30
45
  end
31
46
 
32
47
  def key_history(scope, identifier)
@@ -58,6 +73,7 @@ module Pause
58
73
  # @return count [Integer] the number of items deleted
59
74
  def delete_rate_limited_keys(scope)
60
75
  return 0 unless rate_limited_keys(scope).any?
76
+
61
77
  delete_tracking_keys(scope, rate_limited_keys(scope))
62
78
  redis.zremrangebyscore(rate_limited_list(scope), '-inf', '+inf').tap do |_count|
63
79
  redis.del rate_limited_list(scope)
@@ -70,7 +86,7 @@ module Pause
70
86
  end
71
87
 
72
88
  def disable(scope)
73
- redis.set("internal:|#{scope}|:disabled", "1")
89
+ redis.set("internal:|#{scope}|:disabled", '1')
74
90
  end
75
91
 
76
92
  def enable(scope)
@@ -78,7 +94,7 @@ module Pause
78
94
  end
79
95
 
80
96
  def disabled?(scope)
81
- ! enabled?(scope)
97
+ !enabled?(scope)
82
98
  end
83
99
 
84
100
  def enabled?(scope)
@@ -91,15 +107,21 @@ module Pause
91
107
 
92
108
  private
93
109
 
94
- def delete_tracking_keys(scope, ids)
95
- increment_keys = ids.map{ |key| tracked_key(scope, key) }
96
- redis.del(increment_keys)
110
+ def redis
111
+ self.class.redis
97
112
  end
98
113
 
99
- def redis
100
- @redis_conn ||= ::Redis.new(host: Pause.config.redis_host,
101
- port: Pause.config.redis_port,
102
- db: Pause.config.redis_db)
114
+ def truncate_set_for(k)
115
+ return unless redis.zcard(k) > time_blocks_to_keep
116
+
117
+ list = extract_set_elements(k)
118
+ to_remove = list.slice(0, (list.size - time_blocks_to_keep)).map(&:ts)
119
+ redis.zrem(k, to_remove) if k && to_remove&.size&.positive?
120
+ end
121
+
122
+ def delete_tracking_keys(scope, ids)
123
+ increment_keys = ids.map { |key| tracked_key(scope, key) }
124
+ redis.del(increment_keys)
103
125
  end
104
126
 
105
127
  def tracked_scope(scope)
@@ -117,7 +139,7 @@ module Pause
117
139
 
118
140
  def keys(key_scope)
119
141
  redis.keys("#{key_scope}:*").map do |key|
120
- key.gsub(/^#{key_scope}:/, "").tr('|','')
142
+ key.gsub(/^#{key_scope}:/, '').tr('|', '')
121
143
  end
122
144
  end
123
145
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pause
2
4
  module Redis
3
5
  class OperationNotSupported < StandardError
@@ -7,28 +9,22 @@ module Pause
7
9
  # Operations that are not possible when data is sharded
8
10
  # raise an error.
9
11
  class ShardedAdapter < Adapter
10
- def increment(scope, identifier, timestamp, count = 1)
11
- k = tracked_key(scope, identifier)
12
- redis.zincrby k, count, period_marker(resolution, timestamp)
13
- redis.expire k, history
14
-
15
- if redis.zcard(k) > time_blocks_to_keep
16
- list = extract_set_elements(k)
17
- to_remove = list.slice(0, (list.size - time_blocks_to_keep))
18
- redis.zrem(k, to_remove.map(&:ts))
19
- end
12
+ # Overrides real multi which is not possible when sharded.
13
+ def with_multi
14
+ yield(redis) if block_given?
20
15
  end
21
16
 
17
+ protected
22
18
 
23
- private
24
-
25
- def redis
26
- @redis_conn ||= ::Redis.new(host: Pause.config.redis_host,
27
- port: Pause.config.redis_port)
19
+ def redis_connection_opts
20
+ { host: Pause.config.redis_host,
21
+ port: Pause.config.redis_port }
28
22
  end
29
23
 
24
+ private
25
+
30
26
  def keys(_key_scope)
31
- raise OperationNotSupported.new('Can not be executed when Pause is configured in sharded mode')
27
+ raise OperationNotSupported, 'Can not be executed when Pause is configured in sharded mode'
32
28
  end
33
29
  end
34
30
  end
data/lib/pause/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pause
2
- VERSION = '0.2.1'
4
+ VERSION = '0.5.0'
3
5
  end
data/lib/pause.rb CHANGED
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redis'
4
+ require 'colored2'
2
5
  require 'pause/version'
3
6
  require 'pause/configuration'
4
7
  require 'pause/action'
@@ -9,15 +12,15 @@ require 'pause/redis/sharded_adapter'
9
12
  require 'pause/rate_limited_event'
10
13
 
11
14
  module Pause
12
- class PeriodCheck < Struct.new(:period_seconds, :max_allowed, :block_ttl)
15
+ PeriodCheck = Struct.new(:period_seconds, :max_allowed, :block_ttl) do
13
16
  def <=>(other)
14
- self.period_seconds <=> other.period_seconds
17
+ period_seconds <=> other.period_seconds
15
18
  end
16
19
  end
17
20
 
18
- class SetElement < Struct.new(:ts, :count)
21
+ SetElement = Struct.new(:ts, :count) do
19
22
  def <=>(other)
20
- self.ts <=> other.ts
23
+ ts <=> other.ts
21
24
  end
22
25
  end
23
26
 
@@ -27,21 +30,21 @@ module Pause
27
30
  end
28
31
 
29
32
  def adapter
30
- @adapter ||= config.sharded ?
31
- Pause::Redis::ShardedAdapter.new(config) :
32
- Pause::Redis::Adapter.new(config)
33
+ @adapter ||= if config.sharded
34
+ Pause::Redis::ShardedAdapter.new(config)
35
+ else
36
+ Pause::Redis::Adapter.new(config)
37
+ end
33
38
  end
34
39
 
35
- def adapter=(adapter)
36
- @adapter = adapter
37
- end
40
+ attr_writer :adapter
38
41
 
39
42
  def configure(&block)
40
- @configuration = Pause::Configuration.new.configure(&block)
43
+ @configure ||= Pause::Configuration.new.configure(&block)
41
44
  end
42
45
 
43
- def config
44
- @configuration
46
+ def config(&block)
47
+ configure(&block)
45
48
  end
46
49
  end
47
50
  end
data/pause.gemspec CHANGED
@@ -1,21 +1,27 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ lib = File.expand_path('lib', __dir__)
3
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
6
  require 'pause/version'
5
7
 
6
8
  Gem::Specification.new do |gem|
7
- gem.name = "pause"
9
+ gem.name = 'pause'
8
10
  gem.version = Pause::VERSION
9
- gem.authors = ["Atasay Gokkaya", "Paul Henry", "Eric Saxby", "Konstantin Gredeskoul"]
10
- gem.email = %w(atasay@wanelo.com paul@wanelo.com sax@wanelo.com kig@wanelo.com)
11
- gem.description = %q(Real time rate limiting for multi-process ruby environments based on Redis)
12
- gem.summary = %q(RReal time rate limiting for multi-process ruby environments based on Redis)
13
- gem.homepage = "https://github.com/wanelo/pause"
11
+ gem.authors = ['Konstantin Gredeskoul', 'Atasay Gokkaya', 'Paul Henry', 'Eric Saxby']
12
+ gem.email = %w[kigster@gmail.com atasay@wanelo.com paul@wanelo.com sax@ericsaxby.com]
13
+ gem.summary = 'Fast, scalable, and flexible real time rate limiting library for distributed Ruby environments backed by Redis.'
14
+ gem.description = 'This gem provides highly flexible and easy to use interface to define rate limit checks, register events as they come, and verify if the rate limit is reached. Multiple checks for the same metric are easily supported. This gem is used at very high scale on several popular web sites.'
15
+ gem.homepage = 'https://github.com/kigster/pause'
14
16
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
17
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
+ gem.require_paths = ['lib']
19
20
 
21
+ gem.add_dependency 'colored2'
20
22
  gem.add_dependency 'redis'
23
+
24
+ # optional
25
+ # gem.add_dependency 'hiredis'
26
+ gem.metadata['rubygems_mfa_required'] = 'true'
21
27
  end
@@ -11,6 +11,10 @@ describe Pause::Action do
11
11
  end
12
12
 
13
13
  let(:resolution) { 10 }
14
+ let(:identifier) { '11112222' }
15
+ let(:action) { MyNotification.new(identifier) }
16
+ let(:other_identifier) { '8798734' }
17
+ let(:other_action) { MyNotification.new(other_identifier) }
14
18
  let(:history) { 60 }
15
19
  let(:configuration) { Pause::Configuration.new }
16
20
  let(:adapter) { Pause::Redis::Adapter.new(Pause.config) }
@@ -22,92 +26,178 @@ describe Pause::Action do
22
26
  allow(Pause).to receive(:adapter).and_return(adapter)
23
27
  end
24
28
 
25
- let(:action) { MyNotification.new('1237612') }
26
- let(:other_action) { MyNotification.new('1237613') }
27
-
28
- describe '#increment!' do
29
- it 'should increment' do
30
- time = Time.now
31
- Timecop.freeze time do
32
- expect(Pause.adapter).to receive(:increment).with(action.scope, '1237612', time.to_i, 1)
33
- action.increment!
29
+ RSpec.shared_examples 'an action' do
30
+ describe '#increment!' do
31
+ it 'increments' do
32
+ time = Time.now
33
+ Timecop.freeze time do
34
+ expect(Pause.adapter).to receive(:increment).with(action.scope, identifier, time.to_i, 1)
35
+ action.increment!
36
+ end
34
37
  end
35
38
  end
36
- end
37
39
 
38
- describe '#ok?' do
39
- it 'should successfully return if the action is blocked or not' do
40
- time = Time.now
41
- Timecop.freeze time do
42
- 4.times do
40
+ describe '#ok?' do
41
+ it 'successfullies return if the action is blocked or not' do
42
+ time = Time.now
43
+ Timecop.freeze time do
44
+ 4.times do
45
+ action.increment!
46
+ expect(action.ok?).to be true
47
+ end
43
48
  action.increment!
49
+ expect(action.ok?).to be false
50
+ end
51
+ end
52
+
53
+ it 'successfullies consider different period checks' do
54
+ time = Time.parse('Sept 22, 11:34:00')
55
+
56
+ Timecop.freeze time - 30 do
57
+ action.increment! 4
58
+ expect(action.ok?).to be true
59
+ end
60
+
61
+ Timecop.freeze time do
62
+ action.increment! 2
44
63
  expect(action.ok?).to be true
45
64
  end
46
- action.increment!
65
+
66
+ Timecop.freeze time do
67
+ action.increment! 1
68
+ expect(action.ok?).to be false
69
+ end
70
+ end
71
+
72
+ it 'returns false and silently fail if redis is not available' do
73
+ allow(Pause::Logger).to receive(:fatal)
74
+ allow_any_instance_of(Redis).to receive(:zrange).and_raise Redis::CannotConnectError
75
+ time = period_marker(resolution, Time.now.to_i)
76
+
77
+ action.increment! 4, time - 25
78
+
47
79
  expect(action.ok?).to be false
48
80
  end
49
81
  end
50
82
 
51
- it 'should successfully consider different period checks' do
52
- time = Time.parse('Sept 22, 11:34:00')
83
+ describe '#analyze' do
84
+ context 'action should not be rate limited' do
85
+ it 'returns nil' do
86
+ expect(adapter.rate_limited?(action.scope, action.identifier)).to be false
87
+ expect(action.analyze).to be_nil
88
+ end
89
+ end
53
90
 
54
- Timecop.freeze time - 30 do
55
- action.increment! 4
56
- expect(action.ok?).to be true
91
+ context 'action should be rate limited' do
92
+ it 'returns a RateLimitedEvent object' do
93
+ time = Time.now
94
+ rate_limit = nil
95
+
96
+ Timecop.freeze time do
97
+ 7.times { action.increment! }
98
+ rate_limit = action.analyze
99
+ end
100
+
101
+ expected_rate_limit = Pause::RateLimitedEvent.new(action, action.checks[0], 7, time.to_i)
102
+
103
+ expect(rate_limit).to be_a(Pause::RateLimitedEvent)
104
+ expect(rate_limit.identifier).to eq(expected_rate_limit.identifier)
105
+ expect(rate_limit.sum).to eq(expected_rate_limit.sum)
106
+ expect(rate_limit.period_check).to eq(expected_rate_limit.period_check)
107
+ expect(rate_limit.timestamp).to eq(expected_rate_limit.timestamp)
108
+ end
57
109
  end
110
+ end
58
111
 
59
- Timecop.freeze time do
60
- action.increment! 2
112
+ describe '#unblock' do
113
+ it 'unblocks the specified id' do
114
+ 10.times { action.increment! }
115
+ expect(action.ok?).to be false
116
+ action.unblock
61
117
  expect(action.ok?).to be true
62
118
  end
119
+ end
63
120
 
64
- Timecop.freeze time do
65
- action.increment! 1
121
+ describe '#block_for' do
122
+ it 'blocks the IP for N seconds' do
123
+ expect(adapter).to receive(:rate_limit!).with(action.scope, action.identifier, 10).and_call_original
124
+ action.block_for(10)
66
125
  expect(action.ok?).to be false
67
126
  end
68
127
  end
128
+ end
69
129
 
70
- it 'should return false and silently fail if redis is not available' do
71
- allow(Pause::Logger).to receive(:fatal)
72
- allow_any_instance_of(Redis).to receive(:zrange).and_raise Redis::CannotConnectError
73
- time = period_marker(resolution, Time.now.to_i)
74
-
75
- action.increment! 4, time - 25
76
-
77
- expect(action.ok?).to be false
130
+ context 'actions under test' do
131
+ ['123456', 'hello', 0, 999_999].each do |id|
132
+ let(:identifier) { id }
133
+ let(:action) { MyNotification.new(identifier) }
134
+ describe "action with identifier #{id}" do
135
+ it_behaves_like 'an action'
136
+ end
78
137
  end
79
138
  end
80
139
 
81
- describe '#analyze' do
82
- context 'action should not be rate limited' do
83
- it 'returns nil' do
84
- expect(action.analyze).to be nil
85
- end
140
+ context 'DSL usage' do
141
+ class CowRateLimited < Pause::Action
142
+ scope 'cow:moo'
143
+ check period_seconds: 10, max_allowed: 2, block_ttl: 40
144
+ check period_seconds: 20, max_allowed: 4, block_ttl: 40
86
145
  end
87
146
 
88
- context 'action should be rate limited' do
89
- it 'returns a RateLimitedEvent object' do
90
- time = Time.now
91
- rate_limit = nil
147
+ let(:identifier) { 'cow-moo' }
148
+ let(:action) { CowRateLimited.new(identifier) }
149
+ let(:bogus) { Struct.new(:name, :event).new }
92
150
 
93
- Timecop.freeze time do
94
- 7.times { action.increment! }
95
- rate_limit = action.analyze
151
+ describe '#unless_rate_limited' do
152
+ before { expect(bogus).to receive(:name).twice }
153
+
154
+ describe '#initialize' do
155
+ before { expect(bogus).to receive(:event).once }
156
+
157
+ it 'is able to use methods inside the #new block' do
158
+ b = bogus
159
+ CowRateLimited.new(identifier) do
160
+ unless_rate_limited { b.name } # this executes
161
+ unless_rate_limited { b.name } # this also executes
162
+ unless_rate_limited { b.name } # and this will be rate limited
163
+ if_rate_limited { b.event }
164
+ end
96
165
  end
166
+ end
97
167
 
98
- expected_rate_limit = Pause::RateLimitedEvent.new(action, action.checks[0], 7, time.to_i)
168
+ it 'calls through the block' do
169
+ action.unless_rate_limited { bogus.name }
170
+ action.unless_rate_limited { bogus.name }
171
+ result = action.unless_rate_limited { bogus.name }
172
+ expect(result).to be_a(Pause::RateLimitedEvent)
173
+ end
99
174
 
100
- expect(rate_limit).to be_a(Pause::RateLimitedEvent)
101
- expect(rate_limit.identifier).to eq(expected_rate_limit.identifier)
102
- expect(rate_limit.sum).to eq(expected_rate_limit.sum)
103
- expect(rate_limit.period_check).to eq(expected_rate_limit.period_check)
104
- expect(rate_limit.timestamp).to eq(expected_rate_limit.timestamp)
175
+ it 'calls through the block' do
176
+ 3.times { action.unless_rate_limited { bogus.name } }
177
+ end
178
+
179
+ describe '#if_rate_limited' do
180
+ before { 2.times { action.unless_rate_limited { bogus.name } } }
181
+
182
+ it 'does not analyze during method call' do
183
+ bogus.event = 1
184
+ action.if_rate_limited { |event| bogus.event = event }
185
+ expect(bogus.event).to be_a(Pause::RateLimitedEvent)
186
+ expect(bogus.event.identifier).to eq(identifier)
187
+ end
188
+
189
+ it 'analyzes if requested' do
190
+ action.unless_rate_limited { bogus.name }
191
+ result = action.if_rate_limited { |event| bogus.event = event }
192
+ expect(bogus.event).to be_a(Pause::RateLimitedEvent)
193
+ expect(result).to eq(bogus.event)
194
+ end
105
195
  end
106
196
  end
107
197
  end
108
198
 
109
199
  describe '.tracked_identifiers' do
110
- it 'should return all the identifiers tracked (but not blocked) so far' do
200
+ it 'returns all the identifiers tracked (but not blocked) so far' do
111
201
  action.increment!
112
202
  other_action.increment!
113
203
 
@@ -120,7 +210,7 @@ describe Pause::Action do
120
210
  end
121
211
 
122
212
  describe '.rate_limited_identifiers' do
123
- it 'should return all the identifiers blocked' do
213
+ it 'returns all the identifiers blocked' do
124
214
  action.increment!(100, Time.now.to_i)
125
215
  other_action.increment!(100, Time.now.to_i)
126
216
 
@@ -133,7 +223,7 @@ describe Pause::Action do
133
223
  end
134
224
 
135
225
  describe '.unblock_all' do
136
- it 'should unblock all the identifiers for a scope' do
226
+ it 'unblocks all the identifiers for a scope' do
137
227
  10.times { action.increment! }
138
228
  other_action.increment!
139
229
 
@@ -149,26 +239,6 @@ describe Pause::Action do
149
239
  expect(MyNotification.tracked_identifiers).to eq([other_action.identifier])
150
240
  end
151
241
  end
152
-
153
- describe '#unblock' do
154
- it 'unblocks the specified id' do
155
- 10.times { action.increment! }
156
-
157
- expect(action.ok?).to be false
158
-
159
- action.unblock
160
-
161
- expect(action.ok?).to be true
162
- end
163
- end
164
-
165
- describe '#block_for' do
166
- it 'blocks the IP for N seconds' do
167
- expect(adapter).to receive(:rate_limit!).with(action.scope, action.identifier, 10).and_call_original
168
- action.block_for(10)
169
- expect(action.ok?).to be false
170
- end
171
- end
172
242
  end
173
243
 
174
244
  describe Pause::Action, '.check' do
@@ -186,43 +256,43 @@ describe Pause::Action, '.check' do
186
256
  check period_seconds: 50, block_ttl: 60, max_allowed: 100
187
257
  end
188
258
 
189
- it 'should define a period check on new instances' do
259
+ it 'defines a period check on new instances' do
190
260
  expect(ActionWithCheck.new('id').checks).to eq([
191
- Pause::PeriodCheck.new(100, 150, 200)
192
- ])
261
+ Pause::PeriodCheck.new(100, 150, 200)
262
+ ])
193
263
  end
194
264
 
195
- it 'should define a period check on new instances' do
196
- expect(ActionWithMultipleChecks.new('id').checks).to eq([
197
- Pause::PeriodCheck.new(100, 150, 200),
198
- Pause::PeriodCheck.new(200, 150, 200),
199
- Pause::PeriodCheck.new(300, 150, 200)
200
- ])
265
+ it 'defines a period check on new instances' do
266
+ expect(ActionWithMultipleChecks.new('id').checks).to \
267
+ eq([
268
+ Pause::PeriodCheck.new(100, 150, 200),
269
+ Pause::PeriodCheck.new(200, 150, 200),
270
+ Pause::PeriodCheck.new(300, 150, 200)
271
+ ])
201
272
  end
202
273
 
203
- it 'should accept hash arguments' do
274
+ it 'accepts hash arguments' do
204
275
  expect(ActionWithHashChecks.new('id').checks).to eq([
205
- Pause::PeriodCheck.new(50, 100, 60)
206
- ])
276
+ Pause::PeriodCheck.new(50, 100, 60)
277
+ ])
207
278
  end
208
-
209
279
  end
210
280
 
211
281
  describe Pause::Action, '.scope' do
212
- class UndefinedScopeAction < Pause::Action
282
+ module MyApp
283
+ class NoScope < ::Pause::Action
284
+ end
213
285
  end
214
286
 
215
- it 'should raise if scope is not defined' do
216
- expect {
217
- UndefinedScopeAction.new('1.2.3.4').scope
218
- }.to raise_error('Should implement scope. (Ex: ipn:follow)')
287
+ it 'raises if scope is not defined' do
288
+ expect(MyApp::NoScope.new('1.2.3.4').scope).to eq 'myapp.noscope'
219
289
  end
220
290
 
221
291
  class DefinedScopeAction < Pause::Action
222
292
  scope 'my:scope'
223
293
  end
224
294
 
225
- it 'should set scope on class' do
295
+ it 'sets scope on class' do
226
296
  expect(DefinedScopeAction.new('1.2.3.4').scope).to eq('my:scope')
227
297
  end
228
298
  end
@@ -236,7 +306,7 @@ describe Pause::Action, 'enabled/disabled states' do
236
306
  before do
237
307
  Pause.configure do |c|
238
308
  c.resolution = 10
239
- c.history = 10
309
+ c.history = 10
240
310
  end
241
311
  end
242
312
 
@@ -245,26 +315,26 @@ describe Pause::Action, 'enabled/disabled states' do
245
315
  describe '#disable' do
246
316
  before do
247
317
  expect(action).to be_enabled
248
- expect(action).to_not be_disabled
318
+ expect(action).not_to be_disabled
249
319
  action.disable
250
320
  end
251
321
 
252
322
  it 'disables the action' do
253
323
  expect(action).to be_disabled
254
- expect(action).to_not be_enabled
324
+ expect(action).not_to be_enabled
255
325
  end
256
326
  end
257
327
 
258
328
  describe '#enable' do
259
329
  before do
260
330
  action.disable
261
- expect(action).to_not be_enabled
331
+ expect(action).not_to be_enabled
262
332
  action.enable
263
333
  end
264
334
 
265
335
  it 'enables the action' do
266
336
  expect(action).to be_enabled
267
- expect(action).to_not be_disabled
337
+ expect(action).not_to be_disabled
268
338
  end
269
339
  end
270
340
  end