mirror of
https://github.com/discourse/discourse.git
synced 2024-12-15 10:13:40 +08:00
e25dd41aee
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.
216 lines
5.4 KiB
Ruby
216 lines
5.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
require 'rate_limiter'
|
|
|
|
describe RateLimiter do
|
|
|
|
fab!(:user) { Fabricate(:user) }
|
|
let(:rate_limiter) { RateLimiter.new(user, "peppermint-butler", 2, 60) }
|
|
|
|
context 'disabled' do
|
|
before do
|
|
rate_limiter.performed!
|
|
rate_limiter.performed!
|
|
end
|
|
|
|
it "should be disabled" do
|
|
expect(RateLimiter.disabled?).to eq(true)
|
|
end
|
|
|
|
it "returns true for can_perform?" do
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
|
end
|
|
|
|
it "doesn't raise an error on performed!" do
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
|
|
end
|
|
|
|
context 'enabled' do
|
|
before do
|
|
RateLimiter.enable
|
|
rate_limiter.clear!
|
|
end
|
|
|
|
after do
|
|
RateLimiter.disable
|
|
end
|
|
|
|
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)
|
|
|
|
freeze_time 30.seconds.from_now
|
|
|
|
expect { limiter.performed! }.not_to raise_error
|
|
expect { limiter.performed! }.not_to raise_error
|
|
|
|
end
|
|
end
|
|
|
|
context 'global rate limiter' do
|
|
|
|
it 'can operate in global mode' do
|
|
limiter = RateLimiter.new(nil, "test", 2, 30, global: true)
|
|
limiter.clear!
|
|
|
|
thrown = false
|
|
|
|
limiter.performed!
|
|
limiter.performed!
|
|
begin
|
|
limiter.performed!
|
|
rescue RateLimiter::LimitExceeded => e
|
|
expect(Integer === e.available_in).to eq(true)
|
|
expect(e.available_in).to be > 28
|
|
expect(e.available_in).to be < 32
|
|
thrown = true
|
|
end
|
|
expect(thrown).to be(true)
|
|
end
|
|
|
|
end
|
|
|
|
context 'handles readonly' do
|
|
before do
|
|
Discourse.redis.without_namespace.slaveof '10.0.0.1', '99999'
|
|
end
|
|
|
|
after do
|
|
Discourse.redis.without_namespace.slaveof 'no', 'one'
|
|
end
|
|
|
|
it 'does not explode' do
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context 'never done' do
|
|
it "should perform right away" do
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
|
end
|
|
|
|
it "performs without an error" do
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
context 'max is less than or equal to zero' do
|
|
|
|
it 'should raise the right error' do
|
|
[-1, 0, nil].each do |max|
|
|
expect do
|
|
RateLimiter.new(user, "a", max, 60).performed!
|
|
end.to raise_error(RateLimiter::LimitExceeded)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "multiple calls" do
|
|
before do
|
|
freeze_time
|
|
rate_limiter.performed!
|
|
rate_limiter.performed!
|
|
end
|
|
|
|
it "returns false for can_perform when the limit has been hit" do
|
|
expect(rate_limiter.can_perform?).to eq(false)
|
|
expect(rate_limiter.remaining).to eq(0)
|
|
end
|
|
|
|
it "raises an error the third time called" do
|
|
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
|
|
end
|
|
|
|
context "as an admin/moderator" do
|
|
it "returns true for can_perform if the user is an admin" do
|
|
user.admin = true
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
|
expect(rate_limiter.remaining).to eq(2)
|
|
end
|
|
|
|
it "doesn't raise an error when an admin performs the task" do
|
|
user.admin = true
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
|
|
it "returns true for can_perform if the user is a mod" do
|
|
user.moderator = true
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
|
end
|
|
|
|
it "doesn't raise an error when a moderator performs the task" do
|
|
user.moderator = true
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context "rollback!" do
|
|
before do
|
|
rate_limiter.rollback!
|
|
end
|
|
|
|
it "returns true for can_perform since there is now room" do
|
|
expect(rate_limiter.can_perform?).to eq(true)
|
|
end
|
|
|
|
it "raises no error now that there is room" do
|
|
expect { rate_limiter.performed! }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
end
|