Skip to content

Commit 954eb2a

Browse files
committed
Add error handling spec
1 parent d9bc710 commit 954eb2a

File tree

4 files changed

+233
-4
lines changed

4 files changed

+233
-4
lines changed

lib/rack/attack.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class IncompatibleStoreError < Error; end
3232

3333
THREAD_CALLING_KEY = 'rack.attack.calling'
3434
DEFAULT_FAILURE_COOLDOWN = 60
35-
DEFAULT_IGNORED_ERRORS = %w[Redis::BaseError Dalli::DalliError].freeze
35+
DEFAULT_IGNORED_ERRORS = %w[Dalli::DalliError Redis::BaseError].freeze
3636

3737
class << self
3838
attr_accessor :enabled,
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../spec_helper"
4+
5+
describe "error handling" do
6+
7+
let(:store) do
8+
ActiveSupport::Cache::MemoryStore.new
9+
end
10+
11+
before do
12+
Rack::Attack.cache.store = store
13+
14+
Rack::Attack.blocklist("fail2ban pentesters") do |request|
15+
Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 0, bantime: 600, findtime: 30) { true }
16+
end
17+
end
18+
19+
describe '.ignored_errors' do
20+
before do
21+
allow(store).to receive(:read).and_raise(RuntimeError)
22+
end
23+
24+
it 'has default value' do
25+
assert_equal Rack::Attack.ignored_errors, %w[Dalli::DalliError Redis::BaseError]
26+
end
27+
28+
it 'can get and set value' do
29+
Rack::Attack.ignored_errors = %w[Foobar]
30+
assert_equal Rack::Attack.ignored_errors, %w[Foobar]
31+
end
32+
33+
it 'can ignore error as Class' do
34+
Rack::Attack.ignored_errors = [RuntimeError]
35+
36+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
37+
38+
assert_equal 200, last_response.status
39+
end
40+
41+
it 'can ignore error ancestor as Class' do
42+
Rack::Attack.ignored_errors = [StandardError]
43+
44+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
45+
46+
assert_equal 200, last_response.status
47+
end
48+
49+
it 'can ignore error as String' do
50+
Rack::Attack.ignored_errors = %w[RuntimeError]
51+
52+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
53+
54+
assert_equal 200, last_response.status
55+
end
56+
57+
it 'can ignore error error ancestor as String' do
58+
Rack::Attack.ignored_errors = %w[StandardError]
59+
60+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
61+
62+
assert_equal 200, last_response.status
63+
end
64+
65+
it 'raises error if not ignored' do
66+
assert_raises(RuntimeError) do
67+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
68+
end
69+
end
70+
end
71+
72+
describe '.ignored_errors?' do
73+
74+
it 'can match String or Class' do
75+
Rack::Attack.ignored_errors = ['ArgumentError', RuntimeError]
76+
assert Rack::Attack.ignored_error?(ArgumentError.new)
77+
assert Rack::Attack.ignored_error?(RuntimeError.new)
78+
refute Rack::Attack.ignored_error?(StandardError.new)
79+
end
80+
81+
it 'can match Class ancestors' do
82+
Rack::Attack.ignored_errors = [StandardError]
83+
assert Rack::Attack.ignored_error?(ArgumentError.new)
84+
refute Rack::Attack.ignored_error?(Exception.new)
85+
end
86+
87+
it 'can match String ancestors' do
88+
Rack::Attack.ignored_errors = ['StandardError']
89+
assert Rack::Attack.ignored_error?(ArgumentError.new)
90+
refute Rack::Attack.ignored_error?(Exception.new)
91+
end
92+
end
93+
94+
describe '.error_handler' do
95+
before do
96+
Rack::Attack.error_handler = error_handler if defined?(error_handler)
97+
allow(store).to receive(:read).and_raise(ArgumentError)
98+
end
99+
100+
it 'can get and set value' do
101+
Rack::Attack.error_handler = :test
102+
assert_equal Rack::Attack.error_handler, :test
103+
end
104+
105+
describe 'Proc which returns :block' do
106+
let(:error_handler) { ->(_error) { :block } }
107+
108+
it 'blocks the request' do
109+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
110+
111+
assert_equal 403, last_response.status
112+
end
113+
end
114+
115+
describe 'Proc which returns :throttle' do
116+
let(:error_handler) { ->(_error) { :throttle } }
117+
118+
it 'throttles the request' do
119+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
120+
121+
assert_equal 429, last_response.status
122+
end
123+
end
124+
125+
describe 'Proc which returns :allow' do
126+
let(:error_handler) { ->(_error) { :allow } }
127+
128+
it 'allows the request' do
129+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
130+
131+
assert_equal 200, last_response.status
132+
end
133+
end
134+
135+
describe 'Proc which returns nil' do
136+
let(:error_handler) { ->(_error) { nil } }
137+
138+
it 'allows the request' do
139+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
140+
141+
assert_equal 200, last_response.status
142+
end
143+
end
144+
145+
describe 'Proc which re-raises the error' do
146+
let(:error_handler) { ->(error) { raise error } }
147+
148+
it 'raises the error' do
149+
assert_raises(ArgumentError) do
150+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
151+
end
152+
end
153+
end
154+
155+
describe ':block' do
156+
let(:error_handler) { :block }
157+
158+
it 'blocks the request' do
159+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
160+
161+
assert_equal 403, last_response.status
162+
end
163+
end
164+
165+
describe ':throttle' do
166+
let(:error_handler) { :throttle }
167+
168+
it 'throttles the request' do
169+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
170+
171+
assert_equal 429, last_response.status
172+
end
173+
end
174+
175+
describe ':allow' do
176+
let(:error_handler) { :allow }
177+
178+
it 'allows the request' do
179+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
180+
181+
assert_equal 200, last_response.status
182+
end
183+
end
184+
185+
describe 'non-nil value' do
186+
let(:error_handler) { true }
187+
188+
it 'allows the request' do
189+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
190+
191+
assert_equal 200, last_response.status
192+
end
193+
end
194+
195+
describe 'nil' do
196+
let(:error_handler) { nil }
197+
198+
it 'raises the error' do
199+
assert_raises(ArgumentError) do
200+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
201+
end
202+
end
203+
end
204+
205+
describe 'when error ignored' do
206+
let(:error_handler) { :throttle }
207+
208+
before do
209+
Rack::Attack.ignored_errors = [ArgumentError]
210+
end
211+
212+
it 'calls handler despite ignored error' do
213+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
214+
215+
assert_equal 429, last_response.status
216+
end
217+
end
218+
end
219+
end

spec/acceptance/failure_cooldown_spec.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require_relative "../spec_helper"
44
require "timecop"
55

6-
describe "failure cooldown" do
6+
describe ".failure_cooldown" do
77

88
let(:store) do
99
ActiveSupport::Cache::MemoryStore.new
@@ -22,6 +22,15 @@
2222
end
2323
end
2424

25+
it 'has default value' do
26+
assert_equal Rack::Attack.failure_cooldown, 60
27+
end
28+
29+
it 'can get and set value' do
30+
Rack::Attack.failure_cooldown = 123
31+
assert_equal Rack::Attack.failure_cooldown, 123
32+
end
33+
2534
it "allows requests for 60 seconds after an internal error" do
2635
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
2736

@@ -93,13 +102,13 @@
93102
end
94103
end
95104

96-
describe 'cooldown feature disabled' do
105+
describe 'nil' do
97106

98107
before do
99108
Rack::Attack.failure_cooldown = nil
100109
end
101110

102-
it 'does not activate cooldown' do
111+
it 'disables failure cooldown feature' do
103112
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
104113

105114
assert_equal 403, last_response.status

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class MiniTest::Spec
3737
Rack::Attack.clear_configuration
3838
Rack::Attack.instance_variable_set(:@cache, nil)
3939
Rack::Attack.instance_variable_set(:@last_failure_at, nil)
40+
Rack::Attack.error_handler = nil
4041
Rack::Attack.failure_cooldown = Rack::Attack::DEFAULT_FAILURE_COOLDOWN
4142
Rack::Attack.ignored_errors = Rack::Attack::DEFAULT_IGNORED_ERRORS.dup
4243
end

0 commit comments

Comments
 (0)