towerops/test/towerops_web/plugs/brute_force_protection_test.exs

186 lines
5.6 KiB
Elixir

defmodule ToweropsWeb.Plugs.BruteForceProtectionTest do
use Towerops.DataCase, async: false
import Plug.Conn
alias Towerops.Security.BruteForce
alias ToweropsWeb.Plugs.BruteForceProtection
describe "init/1" do
test "returns opts unchanged" do
assert BruteForceProtection.init([]) == []
end
test "returns arbitrary opts unchanged" do
opts = [threshold: 10, window: 120]
assert BruteForceProtection.init(opts) == opts
end
end
describe "call/2 with non-banned IPs" do
test "allows requests from non-banned IPs" do
conn =
build_conn()
|> put_req_header("x-forwarded-for", "10.0.0.1")
|> BruteForceProtection.call([])
refute conn.halted
assert conn.status != 403
end
test "allows requests from whitelisted IPs even if they would be banned" do
user = Towerops.AccountsFixtures.user_fixture()
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
# Whitelist the IP first
{:ok, _} = BruteForce.add_to_whitelist(ip, "Test whitelist", user)
# Then ban it
{:ok, _block} = BruteForce.create_or_escalate_ban(ip, "Test ban")
conn =
build_conn()
|> put_req_header("x-forwarded-for", ip)
|> BruteForceProtection.call([])
refute conn.halted
end
test "registers before_send callback for 404 tracking" do
conn =
build_conn()
|> put_req_header("x-forwarded-for", "10.0.0.99")
|> BruteForceProtection.call([])
refute conn.halted
# Verify before_send callbacks are registered
assert conn.private[:before_send] != []
end
end
describe "call/2 IP extraction" do
test "extracts IP from x-forwarded-for header" do
conn =
build_conn()
|> put_req_header("x-forwarded-for", "203.0.113.50, 10.0.0.1")
|> BruteForceProtection.call([])
# Should use the first IP (203.0.113.50) and not be banned
refute conn.halted
end
test "extracts IP from x-real-ip header when x-forwarded-for is absent" do
conn =
build_conn()
|> put_req_header("x-real-ip", "203.0.113.60")
|> BruteForceProtection.call([])
refute conn.halted
end
test "falls back to conn.remote_ip when proxy headers are absent" do
conn = BruteForceProtection.call(build_conn(), [])
refute conn.halted
end
end
describe "call/2 with banned IPs" do
@tag :skip
test "blocks requests from banned IPs with 403 and retry-after header" do
# SKIP: Brute force protection is currently disabled (handled by Cloudflare at edge)
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
# Create a ban for the IP (5 minute ban for first offense)
{:ok, _block} = BruteForce.create_or_escalate_ban(ip, "Test ban")
conn =
build_conn()
|> put_req_header("x-forwarded-for", ip)
|> BruteForceProtection.call([])
# When enabled, should halt and return 403
assert conn.halted
assert conn.status == 403
assert get_resp_header(conn, "retry-after") != []
# Verify the IP is actually banned via the context
assert {:blocked, _banned_until} = BruteForce.check_ban_status(ip)
end
test "permanent ban returns retry-after of 86400" do
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
# Escalate to permanent ban (3 offenses)
{:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 1")
{:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 2")
{:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 3")
# Verify permanent ban status via context
assert {:blocked, nil} = BruteForce.check_ban_status(ip)
end
end
describe "call/2 skip branches" do
test "passes through immediately for authenticated users" do
user = Towerops.AccountsFixtures.user_fixture()
conn =
build_conn()
|> assign(:current_user, user)
|> BruteForceProtection.call([])
refute conn.halted
# No before_send callback registered for authenticated users — they
# bypass the 404 tracker entirely.
assert conn.private[:before_send] in [nil, []]
end
test "passes through immediately for agent socket paths" do
conn =
:get
|> Plug.Test.conn("/socket/agent/websocket")
|> BruteForceProtection.call([])
refute conn.halted
assert conn.private[:before_send] in [nil, []]
end
end
describe "ban status integration" do
test "non-banned IP returns :allowed" do
assert :allowed = BruteForce.check_ban_status("10.0.0.99")
end
test "first offense creates a temporary ban" do
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
{:ok, block} = BruteForce.create_or_escalate_ban(ip, "Test ban")
assert block.offense_count == 1
assert block.banned_until
assert {:blocked, banned_until} = BruteForce.check_ban_status(ip)
assert banned_until
end
test "escalation increases offense count" do
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
{:ok, block1} = BruteForce.create_or_escalate_ban(ip, "offense 1")
assert block1.offense_count == 1
{:ok, block2} = BruteForce.create_or_escalate_ban(ip, "offense 2")
assert block2.offense_count == 2
{:ok, block3} = BruteForce.create_or_escalate_ban(ip, "offense 3")
assert block3.offense_count == 3
# Third offense is permanent
assert {:blocked, nil} = BruteForce.check_ban_status(ip)
end
end
defp build_conn do
Plug.Test.conn(:get, "/some/path")
end
end