163 lines
4.4 KiB
Elixir
163 lines
4.4 KiB
Elixir
defmodule ToweropsWeb.Plugs.RateLimitTest do
|
|
use ToweropsWeb.ConnCase, async: false
|
|
|
|
alias ToweropsWeb.Plugs.RateLimit
|
|
|
|
# Use async: false because we're testing shared rate-limiter state
|
|
|
|
setup do
|
|
# Enable rate limiting for this test module (disabled by default in test env)
|
|
Application.put_env(:towerops, :rate_limiting_enabled, true)
|
|
|
|
on_exit(fn ->
|
|
# Restore default test config (disabled)
|
|
Application.put_env(:towerops, :rate_limiting_enabled, false)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
describe "init/1" do
|
|
test "accepts :auth type" do
|
|
assert :auth = RateLimit.init(type: :auth)
|
|
end
|
|
|
|
test "accepts :api type" do
|
|
assert :api = RateLimit.init(type: :api)
|
|
end
|
|
|
|
test "defaults to :api type" do
|
|
assert :api = RateLimit.init([])
|
|
end
|
|
|
|
test "raises on invalid type" do
|
|
assert_raise ArgumentError, ~r/must be :auth, :api, or :admin/, fn ->
|
|
RateLimit.init(type: :invalid)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "call/2 with :auth type" do
|
|
test "allows requests under the limit", %{conn: conn} do
|
|
# Use a unique IP for this test
|
|
unique_ip = "10.0.0.#{:rand.uniform(255)}"
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:auth)
|
|
|
|
refute conn.halted
|
|
end
|
|
|
|
test "blocks requests over the limit", %{conn: conn} do
|
|
# Use a unique IP for this test
|
|
unique_ip = "10.1.0.#{:rand.uniform(255)}"
|
|
|
|
# Make 10 allowed requests (auth limit is 10)
|
|
Enum.each(1..10, fn _ ->
|
|
conn =
|
|
build_conn()
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:auth)
|
|
|
|
refute conn.halted
|
|
end)
|
|
|
|
# 11th request should be blocked
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:auth)
|
|
|
|
assert conn.halted
|
|
assert conn.status == 429
|
|
assert get_resp_header(conn, "retry-after") != []
|
|
|
|
body = Jason.decode!(conn.resp_body)
|
|
assert body["error"] =~ "Too many requests"
|
|
end
|
|
end
|
|
|
|
describe "call/2 with :api type" do
|
|
test "allows requests under the limit", %{conn: conn} do
|
|
unique_ip = "10.2.0.#{:rand.uniform(255)}"
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:api)
|
|
|
|
refute conn.halted
|
|
end
|
|
|
|
test "allows higher limit than auth", %{conn: _conn} do
|
|
unique_ip = "10.3.0.#{:rand.uniform(255)}"
|
|
|
|
# Make 50 requests - should all be allowed (API limit is 100)
|
|
Enum.each(1..50, fn _ ->
|
|
conn =
|
|
build_conn()
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:api)
|
|
|
|
refute conn.halted
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "IP extraction" do
|
|
test "uses X-Forwarded-For header first", %{conn: conn} do
|
|
unique_ip = "192.168.1.#{:rand.uniform(255)}"
|
|
|
|
# Exhaust the limit for this IP
|
|
Enum.each(1..10, fn _ ->
|
|
build_conn()
|
|
|> put_req_header("x-forwarded-for", unique_ip)
|
|
|> RateLimit.call(:auth)
|
|
end)
|
|
|
|
# Different IP should still work
|
|
different_ip = "192.168.2.#{:rand.uniform(255)}"
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-forwarded-for", different_ip)
|
|
|> RateLimit.call(:auth)
|
|
|
|
refute conn.halted
|
|
end
|
|
|
|
test "handles comma-separated X-Forwarded-For", %{conn: conn} do
|
|
unique_ip = "203.0.113.#{:rand.uniform(255)}"
|
|
|
|
# X-Forwarded-For can have multiple IPs (client, proxy1, proxy2)
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-forwarded-for", "#{unique_ip}, 10.0.0.1, 10.0.0.2")
|
|
|> RateLimit.call(:auth)
|
|
|
|
refute conn.halted
|
|
end
|
|
|
|
test "falls back to X-Real-IP", %{conn: conn} do
|
|
unique_ip = "198.51.100.#{:rand.uniform(255)}"
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("x-real-ip", unique_ip)
|
|
|> RateLimit.call(:auth)
|
|
|
|
refute conn.halted
|
|
end
|
|
|
|
test "falls back to remote_ip when no headers", %{conn: conn} do
|
|
# No forwarding headers, uses conn.remote_ip (127.0.0.1)
|
|
# Since other tests might have used this IP, we just verify the plug works
|
|
# by checking it doesn't crash - actual rate limiting is tested elsewhere
|
|
result = RateLimit.call(conn, :auth)
|
|
# Either allowed or blocked is fine - we're testing the IP extraction path
|
|
assert is_struct(result, Plug.Conn)
|
|
end
|
|
end
|
|
end
|