defmodule Plausible.RateLimitTest do use ExUnit.Case, async: true alias Plausible.RateLimit use Plausible.TestUtils @table __MODULE__ defp key, do: "key:#{System.unique_integer([:positive])}" @tag :slow test "garbage collection" do start_supervised!({RateLimit, clean_period: _100_ms = 100, table: @table}) key = key() scale = _50_ms = 50 limit = 10 for _ <- 1..3 do assert {:allow, 1} = RateLimit.check_rate(@table, key, scale, limit) assert [{{^key, _bucket}, counter, expires_at}] = :ets.tab2list(@table) assert :atomics.get(counter, 1) == 1 assert expires_at >= System.system_time(:millisecond) assert expires_at <= System.system_time(:millisecond) + 50 Plausible.TestUtils.eventually(fn -> {:ets.tab2list(@table) == [], :done} end) end end describe "check_rate/3" do setup do start_supervised!({RateLimit, clean_period: :timer.minutes(1), table: @table}) :ok end test "increments" do key = key() scale = :timer.seconds(10) limit = 10 assert {:allow, 1} = RateLimit.check_rate(@table, key, scale, limit) assert {:allow, 2} = RateLimit.check_rate(@table, key, scale, limit) assert {:allow, 3} = RateLimit.check_rate(@table, key, scale, limit) end test "resets" do key = key() scale = 10 limit = 10 assert {:allow, 1} = RateLimit.check_rate(@table, key, scale, limit) :timer.sleep(scale * 2 + 1) assert {:allow, 1} = RateLimit.check_rate(@table, key, scale, limit) end test "denies" do key = key() scale = :timer.seconds(10) limit = 3 assert {:allow, 1} = RateLimit.check_rate(@table, key, scale, limit) assert {:allow, 2} = RateLimit.check_rate(@table, key, scale, limit) assert {:allow, 3} = RateLimit.check_rate(@table, key, scale, limit) assert {:deny, 3} = RateLimit.check_rate(@table, key, scale, limit) end test "denies any concurrent iterations over the limit" do key = key() scale = :timer.seconds(10) limit = 3 assert eventually(fn -> results = 1..4 |> Enum.map(fn _ -> Task.async(fn -> RateLimit.check_rate(@table, key, scale, limit) end) end) |> Task.await_many() |> Enum.sort() {[{:allow, 1}, {:allow, 2}, {:allow, 3}, {:deny, 3}] == results, results} end) end end end