- Add comprehensive i18n design document with domain-based organization
(default, errors, auth, equipment, admin, emails domains)
- Document helper functions, implementation patterns, and migration workflow
- Fix multiple test suite issues:
* user_settings_live_test.exs: Update all tests to match current UI structure
- Fix user.name references (use first_name/last_name)
- Fix TOTP device creation (use create_totp_device/2)
- Update tab URLs and form IDs
- Update tab names (Account, API, Notifications)
- Remove obsolete tabs (Organizations, Activity)
- Fix password tests (pattern match redirect without flash token)
- Update sudo mode behavior
* account_data_controller_test.exs: Fix user profile assertions
* rate_limit_test.exs: Fix unused variable warning
* settings_live_test.exs: Fix organization form validation test
163 lines
4.3 KiB
Elixir
163 lines
4.3 KiB
Elixir
defmodule ToweropsWeb.Plugs.RateLimitTest do
|
|
use ToweropsWeb.ConnCase, async: false
|
|
|
|
alias ToweropsWeb.Plugs.RateLimit
|
|
|
|
# Use async: false because we're testing Hammer 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 or :api/, 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
|