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 = <> network = <> <> = host <> = 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