2019-04-29 20:27:42 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
require 'rate_limiter'
|
|
|
|
|
2022-07-27 22:27:38 -04:00
|
|
|
RSpec.describe RateLimiter do
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2019-05-06 23:12:20 -04:00
|
|
|
fab!(:user) { Fabricate(:user) }
|
2022-02-03 14:07:40 -05:00
|
|
|
fab!(:admin) { Fabricate(:admin) }
|
2013-02-05 14:16:51 -05:00
|
|
|
let(:rate_limiter) { RateLimiter.new(user, "peppermint-butler", 2, 60) }
|
2022-02-03 14:07:40 -05:00
|
|
|
let(:apply_staff_rate_limiter) { RateLimiter.new(admin, "peppermint-servant", 5, 40, apply_limit_to_staff: true) }
|
|
|
|
let(:staff_rate_limiter) { RateLimiter.new(user, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 }) }
|
|
|
|
let(:admin_staff_rate_limiter) { RateLimiter.new(admin, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 }) }
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
context 'disabled' do
|
|
|
|
before do
|
|
|
|
rate_limiter.performed!
|
2013-02-25 11:42:20 -05:00
|
|
|
rate_limiter.performed!
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
it "should be disabled" do
|
|
|
|
expect(RateLimiter.disabled?).to eq(true)
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
it "returns true for can_perform?" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't raise an error on performed!" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'enabled' do
|
|
|
|
before do
|
2017-12-04 05:23:11 -05:00
|
|
|
RateLimiter.enable
|
2013-02-25 11:42:20 -05:00
|
|
|
rate_limiter.clear!
|
2022-02-03 14:07:40 -05:00
|
|
|
staff_rate_limiter.clear!
|
|
|
|
admin_staff_rate_limiter.clear!
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2020-11-05 00:36:17 -05:00
|
|
|
context 'aggressive rate limiter' do
|
|
|
|
|
|
|
|
it 'can operate correctly and totally stop limiting' do
|
|
|
|
|
|
|
|
freeze_time
|
|
|
|
|
|
|
|
# 2 requests every 30 seconds
|
|
|
|
limiter = RateLimiter.new(nil, "test", 2, 30, global: true, aggressive: true)
|
|
|
|
limiter.clear!
|
|
|
|
|
|
|
|
limiter.performed!
|
|
|
|
limiter.performed!
|
|
|
|
freeze_time 29.seconds.from_now
|
|
|
|
|
|
|
|
expect do
|
|
|
|
limiter.performed!
|
|
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
|
|
|
|
|
|
expect do
|
|
|
|
limiter.performed!
|
|
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
|
|
|
|
|
|
# in aggressive mode both these ^^^ count as an attempt
|
|
|
|
freeze_time 29.seconds.from_now
|
|
|
|
|
|
|
|
expect do
|
|
|
|
limiter.performed!
|
|
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
|
|
|
|
|
|
expect do
|
|
|
|
limiter.performed!
|
|
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
|
|
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 13:26:43 -05:00
|
|
|
freeze_time 30.seconds.from_now
|
2020-11-05 00:36:17 -05:00
|
|
|
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 13:26:43 -05:00
|
|
|
expect { limiter.performed! }.not_to raise_error
|
|
|
|
expect { limiter.performed! }.not_to raise_error
|
2020-11-05 00:36:17 -05:00
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
context 'global rate limiter' do
|
|
|
|
|
|
|
|
it 'can operate in global mode' do
|
2017-12-04 05:44:16 -05:00
|
|
|
limiter = RateLimiter.new(nil, "test", 2, 30, global: true)
|
2017-12-04 05:23:11 -05:00
|
|
|
limiter.clear!
|
|
|
|
|
2017-12-04 05:44:16 -05:00
|
|
|
thrown = false
|
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
limiter.performed!
|
|
|
|
limiter.performed!
|
2017-12-04 05:44:16 -05:00
|
|
|
begin
|
|
|
|
limiter.performed!
|
|
|
|
rescue RateLimiter::LimitExceeded => e
|
2017-12-04 15:04:41 -05:00
|
|
|
expect(Integer === e.available_in).to eq(true)
|
2017-12-04 05:44:16 -05:00
|
|
|
expect(e.available_in).to be > 28
|
|
|
|
expect(e.available_in).to be < 32
|
|
|
|
thrown = true
|
|
|
|
end
|
|
|
|
expect(thrown).to be(true)
|
2017-12-04 05:23:11 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'handles readonly' do
|
|
|
|
before do
|
2022-06-21 12:29:36 -04:00
|
|
|
# random IP address in the ULA range that does not exist
|
|
|
|
Discourse.redis.without_namespace.slaveof 'fdec:3f5d:d0b7:4c4b:472b:636a:4370:7ac5', '49999'
|
2017-12-04 05:23:11 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
after do
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.without_namespace.slaveof 'no', 'one'
|
2017-12-04 05:23:11 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not explode' do
|
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
context 'never done' do
|
|
|
|
it "should perform right away" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "performs without an error" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-03-18 11:17:51 -04:00
|
|
|
context "remaining" do
|
|
|
|
it "updates correctly" do
|
|
|
|
expect(rate_limiter.remaining).to eq(2)
|
|
|
|
rate_limiter.performed!
|
|
|
|
expect(rate_limiter.remaining).to eq(1)
|
|
|
|
rate_limiter.performed!
|
|
|
|
expect(rate_limiter.remaining).to eq(0)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-01 00:12:31 -05:00
|
|
|
context 'max is less than or equal to zero' do
|
|
|
|
|
|
|
|
it 'should raise the right error' do
|
2018-03-01 00:20:42 -05:00
|
|
|
[-1, 0, nil].each do |max|
|
2018-03-01 00:12:31 -05:00
|
|
|
expect do
|
|
|
|
RateLimiter.new(user, "a", max, 60).performed!
|
|
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
context "multiple calls" do
|
|
|
|
before do
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 13:26:43 -05:00
|
|
|
freeze_time
|
2013-02-05 14:16:51 -05:00
|
|
|
rate_limiter.performed!
|
|
|
|
rate_limiter.performed!
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns false for can_perform when the limit has been hit" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(false)
|
2016-03-18 11:17:51 -04:00
|
|
|
expect(rate_limiter.remaining).to eq(0)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "raises an error the third time called" do
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 13:26:43 -05:00
|
|
|
expect { rate_limiter.performed! }.to raise_error do |error|
|
|
|
|
expect(error).to be_a(RateLimiter::LimitExceeded)
|
|
|
|
expect(error).to having_attributes(available_in: 60)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises no error when the sliding window ended' do
|
|
|
|
freeze_time 60.seconds.from_now
|
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
context "as an admin/moderator" do
|
|
|
|
it "returns true for can_perform if the user is an admin" do
|
|
|
|
user.admin = true
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
2016-03-18 11:17:51 -04:00
|
|
|
expect(rate_limiter.remaining).to eq(2)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't raise an error when an admin performs the task" do
|
|
|
|
user.admin = true
|
2015-01-09 11:34:37 -05:00
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "returns true for can_perform if the user is a mod" do
|
2013-03-20 00:05:19 -04:00
|
|
|
user.moderator = true
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't raise an error when a moderator performs the task" do
|
2013-03-20 00:05:19 -04:00
|
|
|
user.moderator = true
|
2015-01-09 11:34:37 -05:00
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2022-02-03 14:07:40 -05:00
|
|
|
|
|
|
|
it "applies max / secs to staff when apply_limit_to_staff flag is true" do
|
|
|
|
5.times { apply_staff_rate_limiter.performed! }
|
|
|
|
freeze_time 10.seconds.from_now
|
|
|
|
expect { apply_staff_rate_limiter.performed! }.to raise_error do |error|
|
|
|
|
expect(error).to be_a(RateLimiter::LimitExceeded)
|
|
|
|
expect(error).to having_attributes(available_in: 30)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it "applies staff_limit max when present for staff" do
|
|
|
|
expect(admin_staff_rate_limiter.can_perform?).to eq(true)
|
|
|
|
expect(admin_staff_rate_limiter.remaining).to eq(10)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "applies staff_limit secs when present for staff" do
|
|
|
|
10.times { admin_staff_rate_limiter.performed! }
|
|
|
|
freeze_time 10.seconds.from_now
|
|
|
|
expect { admin_staff_rate_limiter.performed! }.to raise_error do |error|
|
|
|
|
expect(error).to be_a(RateLimiter::LimitExceeded)
|
|
|
|
expect(error).to having_attributes(available_in: 70)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it "applies standard max to non-staff users when staff_limit values are present" do
|
|
|
|
expect(staff_rate_limiter.can_perform?).to eq(true)
|
|
|
|
expect(staff_rate_limiter.remaining).to eq(5)
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
context "rollback!" do
|
|
|
|
before do
|
|
|
|
rate_limiter.rollback!
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns true for can_perform since there is now room" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it "raises no error now that there is room" do
|
2015-01-09 11:34:37 -05:00
|
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-25 11:42:20 -05:00
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|