UNPKG

gip

Version:

Dependency-free, TypeScript-friendly module and CLI tool that uses the Fetch API to retrieve your real public IPv4 address.

137 lines (110 loc) 4.38 kB
import { describe, test, expect, afterEach } from "bun:test"; import gip from "./module.mjs"; import defaultServices from "./services.mjs"; describe("gip module", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); // --- Core behavior --- test("should return a valid public IPv4 address", async () => { const ip = await gip({ ensure: 2, timeout: 15000 }); const IPv4_regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; expect(ip).toMatch(IPv4_regex); }, 20000); test("should resolve instantly and not wait for all fetches to settle", async () => { globalThis.fetch = async (url, opts) => { // Slow services take 5 seconds, fast services respond immediately if (url.includes("ifconfig.me") || url.includes("ipify.org")) { await new Promise((r) => setTimeout(r, 5000)); } return new Response("1.2.3.4"); }; const start = Date.now(); const ip = await gip({ ensure: 1, timeout: 10000 }); const elapsed = Date.now() - start; expect(ip).toBe("1.2.3.4"); // Should resolve well before the slow mocks would respond expect(elapsed).toBeLessThan(2000); }); // --- ensure option --- test("should require ensure count matching IPs before resolving", async () => { let callCount = 0; globalThis.fetch = async () => { callCount++; // All return the same IP; we verify ensure:3 waits for 3 responses return new Response("5.5.5.5"); }; const ip = await gip({ ensure: 3, timeout: 5000 }); expect(ip).toBe("5.5.5.5"); expect(callCount).toBeGreaterThanOrEqual(3); }); test("should throw if ensure count exceeds total number of services", async () => { const totalServices = defaultServices.length; await expect( gip({ ensure: totalServices + 1 }) ).rejects.toThrow(/Maximum ensure count/); }); // --- Services URL formatting --- test("should prepend https:// to custom services without a protocol", async () => { const receivedUrls = []; globalThis.fetch = async (url) => { receivedUrls.push(url); return new Response("7.7.7.7"); }; await gip({ services: ["myip.example.com", "///another.example.com"], ensure: 1, timeout: 2000, }); const customUrls = receivedUrls.filter((u) => u.includes("example.com")); for (const url of customUrls) { expect(url).toMatch(/^https:\/\//); } }); test("should not mutate the original services array passed by the caller", async () => { globalThis.fetch = async () => new Response("8.8.8.8"); const myServices = ["no-protocol.example.com"]; const original = [...myServices]; await gip({ services: myServices, ensure: 1, timeout: 2000 }); expect(myServices).toEqual(original); }); // --- Error cases --- test("should throw if no valid IP is found within the timeout", async () => { globalThis.fetch = async (url, opts) => { // Honour the AbortSignal so gip's controller can cancel this mock await new Promise((resolve, reject) => { const timer = setTimeout(resolve, 5000); opts?.signal?.addEventListener("abort", () => { clearTimeout(timer); reject(Object.assign(new Error("The operation was aborted"), { name: "AbortError" })); }); }); return new Response("1.2.3.4"); }; await expect( gip({ ensure: 1, timeout: 100 }) ).rejects.toThrow(/No valid IP addresses found within/); }); test("should ignore responses that are not valid IPv4 addresses", async () => { let callCount = 0; globalThis.fetch = async () => { callCount++; if (callCount < 5) return new Response("not-an-ip"); return new Response("9.9.9.9"); }; const ip = await gip({ ensure: 1, timeout: 5000 }); expect(ip).toBe("9.9.9.9"); }); test("should ignore addresses outside valid IPv4 range", async () => { let callCount = 0; globalThis.fetch = async () => { callCount++; if (callCount < 3) return new Response("999.999.999.999"); return new Response("10.0.0.1"); }; const ip = await gip({ ensure: 1, timeout: 5000 }); expect(ip).toBe("10.0.0.1"); }); });