1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
|
# frozen_string_literal: true
require_relative "../spec_helper"
require "timecop"
describe "#throttle" do
before do
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
end
it "allows one request per minute by IP" do
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
request.ip
end
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 429, last_response.status
assert_nil last_response.headers["Retry-After"]
assert_equal "Retry later\n", last_response.body
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
assert_equal 200, last_response.status
Timecop.travel(60) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
end
end
it "returns correct Retry-After header if enabled" do
Rack::Attack.throttled_response_retry_after_header = true
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
request.ip
end
Timecop.freeze(Time.at(0)) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
end
Timecop.freeze(Time.at(25)) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal "35", last_response.headers["Retry-After"]
end
end
it "supports limit to be dynamic" do
# Could be used to have different rate limits for authorized
# vs general requests
limit_proc = lambda do |request|
if request.env["X-APIKey"] == "private-secret"
2
else
1
end
end
Rack::Attack.throttle("by ip", limit: limit_proc, period: 60) do |request|
request.ip
end
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 429, last_response.status
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 429, last_response.status
end
it "supports period to be dynamic" do
# Could be used to have different rate limits for authorized
# vs general requests
period_proc = lambda do |request|
if request.env["X-APIKey"] == "private-secret"
10
else
30
end
end
Rack::Attack.throttle("by ip", limit: 1, period: period_proc) do |request|
request.ip
end
# Using Time#at to align to start/end of periods exactly
# to achieve consistenty in different test runs
Timecop.travel(Time.at(0)) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 429, last_response.status
end
Timecop.travel(Time.at(10)) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 429, last_response.status
end
Timecop.travel(Time.at(30)) do
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
end
Timecop.travel(Time.at(0)) do
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 200, last_response.status
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 429, last_response.status
end
Timecop.travel(Time.at(10)) do
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
assert_equal 200, last_response.status
end
end
it "notifies when the request is throttled" do
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
request.ip
end
notification_matched = nil
notification_type = nil
notification_data = nil
notification_discriminator = nil
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |_name, _start, _finish, _id, payload|
notification_matched = payload[:request].env["rack.attack.matched"]
notification_type = payload[:request].env["rack.attack.match_type"]
notification_data = payload[:request].env['rack.attack.match_data']
notification_discriminator = payload[:request].env['rack.attack.match_discriminator']
end
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
assert_equal 200, last_response.status
assert_nil notification_matched
assert_nil notification_type
assert_nil notification_data
assert_nil notification_discriminator
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 200, last_response.status
assert_nil notification_matched
assert_nil notification_type
assert_nil notification_data
assert_nil notification_discriminator
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
assert_equal 429, last_response.status
assert_equal "by ip", notification_matched
assert_equal :throttle, notification_type
assert_equal 60, notification_data[:period]
assert_equal 1, notification_data[:limit]
assert_equal 2, notification_data[:count]
assert_equal "1.2.3.4", notification_discriminator
end
end
|