import { describe, expect, it } from "bun:test"; import { resolveTrustedClientIp, isIpAddress, isLoopbackAddress, UNKNOWN_CLIENT_IP, type ClientIpEventLike } from "./trusted-client-ip"; /** * Unit coverage for the security-path client-IP read (FRE-371). The caller * suites (rate-limit.test.ts, report.test.ts) pin the behaviour through * getClientIP and the route; this file pins the helper's own edges: the peer * forms a real socket can present, the APIEvent wrapper, and failing closed. */ function event(opts: { headers?: Record; remoteAddress?: string; wrapped?: boolean; }): ClientIpEventLike { const inner: ClientIpEventLike = { ...(opts.remoteAddress ? { node: { req: { socket: { remoteAddress: opts.remoteAddress } } } } : {}), request: { headers: new Headers(opts.headers ?? {}) } }; // An APIEvent wraps its H3Event as `nativeEvent` (routes/api/* shape). return opts.wrapped ? { nativeEvent: inner } : inner; } describe("resolveTrustedClientIp", () => { it("trust order: non-loopback peer, then cf-connecting-ip, then rightmost-minus-one x-forwarded-for", () => { // Peer wins. expect( resolveTrustedClientIp( event({ remoteAddress: "198.51.100.23", headers: { "cf-connecting-ip": "203.0.113.7", "x-forwarded-for": "6.6.6.6, 203.0.113.7, 10.0.0.1" } }) ) ).toBe("198.51.100.23"); // Loopback peer → Cloudflare's record. expect( resolveTrustedClientIp( event({ remoteAddress: "127.0.0.1", headers: { "cf-connecting-ip": "203.0.113.7", "x-forwarded-for": "6.6.6.6, 203.0.113.7, 10.0.0.1" } }) ) ).toBe("203.0.113.7"); // No CF header → the hop below the trusted proxy. expect( resolveTrustedClientIp( event({ remoteAddress: "127.0.0.1", headers: { "x-forwarded-for": "6.6.6.6, 203.0.113.7, 10.0.0.1" } }) ) ).toBe("203.0.113.7"); }); it("reads the wrapped H3Event of an APIEvent-shaped input", () => { expect( resolveTrustedClientIp( event({ wrapped: true, remoteAddress: "127.0.0.1", headers: { "cf-connecting-ip": "203.0.113.7" } }) ) ).toBe("203.0.113.7"); }); it("never returns a truncated address", () => { // The full /128 is the bucket key; a truncated one would pool attackers // with their neighbours. const ip = resolveTrustedClientIp( event({ remoteAddress: "127.0.0.1", headers: { "cf-connecting-ip": "2001:0db8:85a3:0000:0000:8a2e:0370:7334" } }) ); expect(ip).toBe("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); }); it("fails closed to the stricter unknown key", () => { // One hop = the sender's own invention; a non-address hop; nothing at all. expect( resolveTrustedClientIp( event({ headers: { "x-forwarded-for": "6.6.6.6" } }) ) ).toBe(UNKNOWN_CLIENT_IP); expect( resolveTrustedClientIp( event({ headers: { "cf-connecting-ip": "not-an-ip" } }) ) ).toBe(UNKNOWN_CLIENT_IP); expect(resolveTrustedClientIp(event({}))).toBe(UNKNOWN_CLIENT_IP); }); it("recognises the loopback forms a real peer presents", () => { expect( resolveTrustedClientIp( event({ // IPv6-mapped IPv4 loopback from a node socket. remoteAddress: "::ffff:127.0.0.1", headers: { "cf-connecting-ip": "203.0.113.7" } }) ) ).toBe("203.0.113.7"); expect( resolveTrustedClientIp( event({ remoteAddress: "::1", headers: { "cf-connecting-ip": "203.0.113.7" } }) ) ).toBe("203.0.113.7"); // A non-loopback peer that merely looks odd is still the client. expect( resolveTrustedClientIp( event({ remoteAddress: "::ffff:203.0.113.7", headers: { "cf-connecting-ip": "6.6.6.6" } }) ) ).toBe("203.0.113.7"); }); it("never believes x-real-ip", () => { expect( resolveTrustedClientIp( event({ headers: { "x-real-ip": "203.0.113.7" } }) ) ).toBe(UNKNOWN_CLIENT_IP); }); }); describe("address helpers", () => { it("isIpAddress", () => { expect(isIpAddress("203.0.113.7")).toBe(true); expect(isIpAddress("::1")).toBe(true); expect(isIpAddress("::ffff:203.0.113.7")).toBe(true); expect(isIpAddress("2001:db8::1")).toBe(true); expect(isIpAddress("not-an-ip")).toBe(false); expect(isIpAddress("256.1.1.1")).toBe(false); expect(isIpAddress("")).toBe(false); expect(isIpAddress(null)).toBe(false); expect(isIpAddress(undefined)).toBe(false); }); it("isLoopbackAddress", () => { expect(isLoopbackAddress("127.0.0.1")).toBe(true); expect(isLoopbackAddress("127.255.0.3")).toBe(true); expect(isLoopbackAddress("::1")).toBe(true); expect(isLoopbackAddress("::ffff:127.0.0.1")).toBe(true); expect(isLoopbackAddress("203.0.113.7")).toBe(false); expect(isLoopbackAddress("::2")).toBe(false); // Unparseable input is not loopback — fails closed. expect(isLoopbackAddress("garbage")).toBe(false); }); });