Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
85 lines
2.5 KiB
Elixir
85 lines
2.5 KiB
Elixir
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
|