# frozen_string_literal: true require "rate_limiter" RSpec.describe RateLimiter do fab!(:user) { Fabricate(:user) } fab!(:admin) { Fabricate(:admin) } let(:rate_limiter) { RateLimiter.new(user, "peppermint-butler", 2, 60) } let(:apply_staff_rate_limiter) do RateLimiter.new(admin, "peppermint-servant", 5, 40, apply_limit_to_staff: true) end let(:staff_rate_limiter) do RateLimiter.new(user, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 }) end let(:admin_staff_rate_limiter) do RateLimiter.new(admin, "peppermind-servant", 5, 40, staff_limit: { max: 10, secs: 80 }) end describe "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 describe "enabled" do before do RateLimiter.enable rate_limiter.clear! staff_rate_limiter.clear! admin_staff_rate_limiter.clear! end context "with 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 "with 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 "when handling readonly" do before do # 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" end after { Discourse.redis.without_namespace.slaveof "no", "one" } it "does not explode" do expect { rate_limiter.performed! }.not_to raise_error end end context "when 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 "when 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 "when 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 "with 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 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 end describe "#rollback!" do before { rate_limiter.rollback! } 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