defmodule ToweropsWeb.RemoteIpTest do use ToweropsWeb.ConnCase, async: true alias ToweropsWeb.RemoteIp describe "from_conn/1" do test "extracts the first forwarded IP when from a trusted proxy (10.x)", %{conn: conn} do conn = conn |> Map.put(:remote_ip, {10, 0, 0, 1}) |> put_req_header("x-forwarded-for", "203.0.113.10, 10.0.0.1") assert RemoteIp.from_conn(conn) == "203.0.113.10" end test "extracts the first forwarded IP when from a trusted proxy (172.16-31.x)", %{conn: conn} do conn = conn |> Map.put(:remote_ip, {172, 16, 0, 1}) |> put_req_header("x-forwarded-for", "198.51.100.1, 172.16.0.1") assert RemoteIp.from_conn(conn) == "198.51.100.1" end test "extracts the first forwarded IP when from a trusted proxy (192.168.x)", %{conn: conn} do conn = conn |> Map.put(:remote_ip, {192, 168, 1, 1}) |> put_req_header("x-forwarded-for", "198.51.100.1") assert RemoteIp.from_conn(conn) == "198.51.100.1" end test "uses x-real-ip when x-forwarded-for is missing and from trusted proxy", %{conn: conn} do conn = conn |> Map.put(:remote_ip, {10, 0, 0, 1}) |> put_req_header("x-real-ip", "198.51.100.1") assert RemoteIp.from_conn(conn) == "198.51.100.1" end test "uses remote_ip directly when neither forwarded header is present and from trusted proxy", %{conn: conn} do conn = Map.put(conn, :remote_ip, {10, 0, 0, 1}) assert RemoteIp.from_conn(conn) == "10.0.0.1" end test "uses remote_ip directly when NOT from a trusted proxy", %{conn: conn} do # 8.8.8.8 is a public IP, not a trusted proxy — x-forwarded-for is ignored conn = conn |> Map.put(:remote_ip, {8, 8, 8, 8}) |> put_req_header("x-forwarded-for", "203.0.113.10") assert RemoteIp.from_conn(conn) == "8.8.8.8" end test "trims whitespace from forwarded IP", %{conn: conn} do conn = conn |> Map.put(:remote_ip, {10, 0, 0, 1}) |> put_req_header("x-forwarded-for", " 203.0.113.10 , 10.0.0.1") assert RemoteIp.from_conn(conn) == "203.0.113.10" end end describe "format/1" do test "formats IPv4 tuple" do assert RemoteIp.format({192, 168, 1, 1}) == "192.168.1.1" assert RemoteIp.format({10, 0, 0, 1}) == "10.0.0.1" assert RemoteIp.format({0, 0, 0, 0}) == "0.0.0.0" end test "formats IPv6 addresses in uppercase hex" do assert RemoteIp.format({8193, 3512, 0, 0, 0, 0, 0, 1}) == "2001:DB8:0:0:0:0:0:1" end test "formats loopback IPv6" do assert RemoteIp.format({0, 0, 0, 0, 0, 0, 0, 1}) == "0:0:0:0:0:0:0:1" end test "returns nil for invalid tuple" do assert RemoteIp.format(:error) == nil assert RemoteIp.format("not-a-tuple") == nil assert RemoteIp.format(nil) == nil end end describe "from_socket/1" do test "returns formatted IP from peer_data in assigns" do socket = %Phoenix.Socket{ assigns: %{peer_data: %{address: {192, 168, 1, 42}, port: 12_345}} } assert RemoteIp.from_socket(socket) == "192.168.1.42" end test "returns nil when peer_data is missing" do socket = %Phoenix.Socket{assigns: %{}} assert RemoteIp.from_socket(socket) == nil end test "returns nil when peer_data has no address tuple" do socket = %Phoenix.Socket{assigns: %{peer_data: %{}}} assert RemoteIp.from_socket(socket) == nil end end end