defmodule TarakanWeb.Plugs.ApiRateLimitTest do use Tarakan.DataCase, async: true import Plug.Conn import Plug.Test alias Tarakan.Accounts.Scope alias TarakanWeb.Plugs.ApiRateLimit defp call_ip(conn, opts) do ApiRateLimit.call(conn, ApiRateLimit.init([mode: :ip] ++ opts)) end defp call_actor(conn, scope, opts) do conn |> assign(:current_scope, scope) |> ApiRateLimit.call(ApiRateLimit.init([mode: :actor] ++ opts)) end defp credential_scope(account, token_id) do Scope.for_account(account, token_id: token_id, authentication_method: :api_credential) end test "requests carrying a Bearer token count against the IP bucket" do opts = [request_limit: 2, mutation_limit: 2, window_seconds: 60] for _ <- 1..2 do conn = conn(:get, "/api/repositories") |> put_req_header("authorization", "Bearer garbage-token") |> call_ip(opts) refute conn.halted end conn = conn(:get, "/api/repositories") |> put_req_header("authorization", "Bearer garbage-token") |> call_ip(opts) assert conn.halted assert conn.status == 429 assert Jason.decode!(conn.resp_body)["error"] == "rate limit exceeded" end test "IPv6 clients in the same /64 share one IP bucket" do opts = [request_limit: 1, mutation_limit: 1, window_seconds: 60] first = conn(:get, "/api/repositories") |> Map.put(:remote_ip, {0x2001, 0x0DB8, 0, 0, 0, 0, 0, 1}) |> call_ip(opts) refute first.halted second = conn(:get, "/api/repositories") |> Map.put(:remote_ip, {0x2001, 0x0DB8, 0, 0, 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD}) |> call_ip(opts) assert second.halted assert second.status == 429 end test "plain requests still count against the IP bucket" do opts = [request_limit: 1, mutation_limit: 1, window_seconds: 60] refute conn(:get, "/api/repositories") |> call_ip(opts) |> Map.fetch!(:halted) assert conn(:get, "/api/repositories") |> call_ip(opts) |> Map.fetch!(:halted) end test "authenticated actors get the multiplied actor budget" do opts = [request_limit: 2, mutation_limit: 2, window_seconds: 60] scope = credential_scope(account_fixture(), 11_001) # 2 × @actor_multiplier (4) = 8 requests fit; the 9th is rejected. for _ <- 1..8 do refute conn(:get, "/api/jobs") |> call_actor(scope, opts) |> Map.fetch!(:halted) end limited = conn(:get, "/api/jobs") |> call_actor(scope, opts) assert limited.halted assert limited.status == 429 end end