pause 0.2.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/rspec.yml +31 -0
- data/.github/workflows/rubocop.yml +31 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +192 -0
- data/Gemfile +16 -9
- data/Gemfile.lock +145 -0
- data/Guardfile +7 -6
- data/LICENSE.txt +1 -1
- data/README.md +152 -27
- data/Rakefile +44 -2
- data/bin/spec +14 -0
- data/lib/pause/action.rb +116 -94
- data/lib/pause/analyzer.rb +7 -3
- data/lib/pause/configuration.rb +4 -2
- data/lib/pause/helper/timing.rb +2 -0
- data/lib/pause/logger.rb +12 -5
- data/lib/pause/rate_limited_event.rb +2 -1
- data/lib/pause/redis/adapter.rb +41 -19
- data/lib/pause/redis/sharded_adapter.rb +12 -16
- data/lib/pause/version.rb +3 -1
- data/lib/pause.rb +16 -13
- data/pause.gemspec +18 -12
- data/spec/pause/action_spec.rb +168 -98
- data/spec/pause/analyzer_spec.rb +9 -10
- data/spec/pause/configuration_spec.rb +6 -5
- data/spec/pause/logger_spec.rb +25 -0
- data/spec/pause/pause_spec.rb +6 -4
- data/spec/pause/redis/adapter_spec.rb +56 -31
- data/spec/pause/redis/sharded_adapter_spec.rb +19 -5
- data/spec/spec_helper.rb +24 -11
- metadata +39 -27
- data/.travis.yml +0 -10
- data/spec/support/fakeredis.rb +0 -2
data/lib/pause/redis/adapter.rb
CHANGED
@@ -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
|
25
|
+
@resolution = config.resolution
|
14
26
|
@time_blocks_to_keep = config.history / @resolution
|
15
|
-
@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
|
-
|
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
|
-
|
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",
|
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
|
-
!
|
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
|
95
|
-
|
96
|
-
redis.del(increment_keys)
|
110
|
+
def redis
|
111
|
+
self.class.redis
|
97
112
|
end
|
98
113
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
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}:/,
|
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
|
-
|
11
|
-
|
12
|
-
redis
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
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
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
|
-
|
15
|
+
PeriodCheck = Struct.new(:period_seconds, :max_allowed, :block_ttl) do
|
13
16
|
def <=>(other)
|
14
|
-
|
17
|
+
period_seconds <=> other.period_seconds
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
18
|
-
|
21
|
+
SetElement = Struct.new(:ts, :count) do
|
19
22
|
def <=>(other)
|
20
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
36
|
-
@adapter = adapter
|
37
|
-
end
|
40
|
+
attr_writer :adapter
|
38
41
|
|
39
42
|
def configure(&block)
|
40
|
-
@
|
43
|
+
@configure ||= Pause::Configuration.new.configure(&block)
|
41
44
|
end
|
42
45
|
|
43
|
-
def config
|
44
|
-
|
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
|
-
#
|
2
|
-
|
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 =
|
9
|
+
gem.name = 'pause'
|
8
10
|
gem.version = Pause::VERSION
|
9
|
-
gem.authors = [
|
10
|
-
gem.email = %w
|
11
|
-
gem.
|
12
|
-
gem.
|
13
|
-
gem.homepage =
|
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.
|
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
|
data/spec/pause/action_spec.rb
CHANGED
@@ -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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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 '
|
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 '
|
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 '
|
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 '
|
259
|
+
it 'defines a period check on new instances' do
|
190
260
|
expect(ActionWithCheck.new('id').checks).to eq([
|
191
|
-
|
192
|
-
|
261
|
+
Pause::PeriodCheck.new(100, 150, 200)
|
262
|
+
])
|
193
263
|
end
|
194
264
|
|
195
|
-
it '
|
196
|
-
expect(ActionWithMultipleChecks.new('id').checks).to
|
197
|
-
|
198
|
-
|
199
|
-
|
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 '
|
274
|
+
it 'accepts hash arguments' do
|
204
275
|
expect(ActionWithHashChecks.new('id').checks).to eq([
|
205
|
-
|
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
|
-
|
282
|
+
module MyApp
|
283
|
+
class NoScope < ::Pause::Action
|
284
|
+
end
|
213
285
|
end
|
214
286
|
|
215
|
-
it '
|
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 '
|
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
|
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).
|
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).
|
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).
|
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).
|
337
|
+
expect(action).not_to be_disabled
|
268
338
|
end
|
269
339
|
end
|
270
340
|
end
|