prop/test/microwaveprop_web/plugs/remote_ip_property_test.exs

141 lines
4.5 KiB
Elixir

defmodule MicrowavepropWeb.Plugs.RemoteIpPropertyTest do
@moduledoc """
Property tests for the CIDR-matching logic in
`MicrowavepropWeb.Plugs.RemoteIp`. The `peer_trusted?` helper is
private, so we exercise it via the plug's `init/1` + `call/2`
pipeline: set up a trusted-proxy list, hand the plug a conn whose
`remote_ip` we control, and assert whether the forwarded-IP header
was honoured.
"""
use ExUnit.Case, async: true
use ExUnitProperties
import ExUnit.CaptureLog
alias MicrowavepropWeb.Plugs.RemoteIp
# The X-Forwarded-For value we supply whenever we want to check
# whether the peer was trusted.
@forwarded_ipv4 "198.51.100.7"
@forwarded_tuple {198, 51, 100, 7}
# ── Generators ──────────────────────────────────────────────────
defp ipv4_octet, do: integer(0..255)
defp ipv4_tuple_gen do
gen all(
a <- ipv4_octet(),
b <- ipv4_octet(),
c <- ipv4_octet(),
d <- ipv4_octet()
) do
{a, b, c, d}
end
end
defp ipv4_string(tuple) do
tuple |> Tuple.to_list() |> Enum.join(".")
end
# ── Helpers ─────────────────────────────────────────────────────
defp call_plug(remote_ip, trusted_cidrs) do
opts = RemoteIp.init(trusted_proxies: trusted_cidrs)
conn =
Phoenix.ConnTest.build_conn()
|> Map.put(:remote_ip, remote_ip)
|> Plug.Conn.put_req_header("x-forwarded-for", @forwarded_ipv4)
RemoteIp.call(conn, opts)
end
defp trusted?(remote_ip, cidrs) do
call_plug(remote_ip, cidrs).remote_ip == @forwarded_tuple
end
defp in_cidr?({a, b, c, d}, {{na, nb, nc, nd}, prefix}) do
host = <<a, b, c, d>>
network = <<na, nb, nc, nd>>
<<host_prefix::bitstring-size(prefix), _::bitstring>> = host
<<net_prefix::bitstring-size(prefix), _::bitstring>> = network
host_prefix == net_prefix
end
# ── Properties ──────────────────────────────────────────────────
property "/0 trusts every IPv4 peer" do
check all(peer <- ipv4_tuple_gen()) do
assert trusted?(peer, ["0.0.0.0/0"])
end
end
property "/32 trusts only the exact IP" do
check all(
anchor <- ipv4_tuple_gen(),
other <- ipv4_tuple_gen(),
anchor != other
) do
cidrs = [ipv4_string(anchor) <> "/32"]
assert trusted?(anchor, cidrs)
refute trusted?(other, cidrs)
end
end
property "every X.Y.Z.W in 10.0.0.0/8 is trusted; nothing outside is" do
cidrs = ["10.0.0.0/8"]
check all(ip <- ipv4_tuple_gen()) do
inside = in_cidr?(ip, {{10, 0, 0, 0}, 8})
assert trusted?(ip, cidrs) == inside
end
end
property "arbitrary /prefix CIDRs match iff the peer's high-order prefix bits agree" do
check all(
network <- ipv4_tuple_gen(),
prefix <- integer(1..32),
peer <- ipv4_tuple_gen()
) do
cidr = ipv4_string(network) <> "/" <> Integer.to_string(prefix)
assert trusted?(peer, [cidr]) == in_cidr?(peer, {network, prefix})
end
end
property "union of multiple CIDRs matches the logical OR of each" do
check all(
a <- ipv4_tuple_gen(),
prefix_a <- integer(4..32),
b <- ipv4_tuple_gen(),
prefix_b <- integer(4..32),
peer <- ipv4_tuple_gen()
) do
cidrs = [
ipv4_string(a) <> "/" <> Integer.to_string(prefix_a),
ipv4_string(b) <> "/" <> Integer.to_string(prefix_b)
]
expected = in_cidr?(peer, {a, prefix_a}) or in_cidr?(peer, {b, prefix_b})
assert trusted?(peer, cidrs) == expected
end
end
property "/0 as the only CIDR still trusts everything even with bogus entries mixed in" do
# Invalid CIDR strings are dropped at init time with a warning;
# the surviving /0 should still match every peer.
capture_log(fn ->
check all(peer <- ipv4_tuple_gen()) do
assert trusted?(peer, ["0.0.0.0/0", "not-a-cidr", "999.999.999.999/24", "10.0.0.0/33"])
end
end)
end
property "an empty trusted-proxy list rejects every peer" do
check all(peer <- ipv4_tuple_gen()) do
# No CIDRs → the forwarded header must not flow through.
refute trusted?(peer, [])
end
end
end