defmodule ToweropsWeb.Plugs.RemoteIpLoggerTest do use ToweropsWeb.ConnCase, async: true alias ToweropsWeb.Plugs.RemoteIpLogger require Logger describe "init/1" do test "returns options unchanged" do assert RemoteIpLogger.init([]) == [] assert RemoteIpLogger.init(some: :option) == [some: :option] end end describe "call/2" do test "extracts IP from X-Forwarded-For header" do build_conn() |> put_req_header("x-forwarded-for", "203.0.113.195") |> RemoteIpLogger.call([]) assert Logger.metadata()[:remote_ip] == "203.0.113.195" end test "extracts first IP from comma-separated X-Forwarded-For" do build_conn() |> put_req_header("x-forwarded-for", "203.0.113.195, 10.0.0.1, 192.168.1.1") |> RemoteIpLogger.call([]) assert Logger.metadata()[:remote_ip] == "203.0.113.195" end test "trims whitespace from X-Forwarded-For IP" do build_conn() |> put_req_header("x-forwarded-for", " 203.0.113.195 , 10.0.0.1") |> RemoteIpLogger.call([]) assert Logger.metadata()[:remote_ip] == "203.0.113.195" end test "falls back to X-Real-IP when X-Forwarded-For is not present" do build_conn() |> put_req_header("x-real-ip", "198.51.100.42") |> RemoteIpLogger.call([]) assert Logger.metadata()[:remote_ip] == "198.51.100.42" end test "formats IPv4 from conn.remote_ip when no proxy headers present" do # ConnCase builds a conn with remote_ip {127, 0, 0, 1} RemoteIpLogger.call(build_conn(), []) assert Logger.metadata()[:remote_ip] == "127.0.0.1" end test "formats IPv6 from conn.remote_ip" do build_conn() |> Map.put(:remote_ip, {8193, 3512, 0, 0, 0, 0, 0, 1}) |> RemoteIpLogger.call([]) # IPv6 formatted as hex segments separated by colons (uppercase) assert Logger.metadata()[:remote_ip] == "2001:DB8:0:0:0:0:0:1" end test "prefers X-Forwarded-For over X-Real-IP" do build_conn() |> put_req_header("x-forwarded-for", "203.0.113.195") |> put_req_header("x-real-ip", "198.51.100.42") |> RemoteIpLogger.call([]) assert Logger.metadata()[:remote_ip] == "203.0.113.195" end test "returns conn unchanged" do original_conn = build_conn() result_conn = RemoteIpLogger.call(original_conn, []) # Conn should pass through unchanged (only metadata is modified) assert result_conn.adapter == original_conn.adapter assert result_conn.method == original_conn.method assert result_conn.path_info == original_conn.path_info end end end