From 176f76ef5b8de9025acc47de7e6055df64dfa5ac Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 24 Nov 2025 18:24:30 +0100 Subject: [PATCH] Add support for Plus bundle --- __tests__/ipinfo-plus-middleware.test.ts | 115 +++++++++++++++++++++++ src/index.ts | 33 ++++++- 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 __tests__/ipinfo-plus-middleware.test.ts diff --git a/__tests__/ipinfo-plus-middleware.test.ts b/__tests__/ipinfo-plus-middleware.test.ts new file mode 100644 index 0000000..13a8fc9 --- /dev/null +++ b/__tests__/ipinfo-plus-middleware.test.ts @@ -0,0 +1,115 @@ +import { Request, Response, NextFunction } from "express"; +import { ipinfoPlus, originatingIPSelector } from "../src/index"; +import { IPinfoPlus } from "node-ipinfo/dist/src/common"; + +// Mock the node-ipinfo module +const mockLookupIp = jest.fn(); +jest.mock("node-ipinfo", () => ({ + IPinfoPlusWrapper: jest.fn().mockImplementation(() => ({ + lookupIp: mockLookupIp + })) +})); + +describe("ipinfoPlusMiddleware", () => { + const mockToken = "test_token"; + let mockReq: Partial & { ipinfo?: IPinfoPlus }; + let mockRes: Partial; + let next: NextFunction; + + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + + // Set up default mock response + mockLookupIp.mockResolvedValue({ + ip: "1.2.3.4", + city: "New York", + country: "US", + hostname: "example.com", + org: "Example Org" + }); + + // Setup mock request/response + mockReq = { + ip: "1.2.3.4", + headers: { "x-forwarded-for": "5.6.7.8, 10.0.0.1" }, + header: jest.fn((name: string) => { + if (name.toLowerCase() === "set-cookie") { + return ["mock-cookie-1", "mock-cookie-2"]; + } + if (name.toLowerCase() === "x-forwarded-for") { + return "5.6.7.8, 10.0.0.1"; + } + return undefined; + }) as jest.MockedFunction< + ((name: "set-cookie") => string[] | undefined) & + ((name: string) => string | undefined) + > + }; + mockRes = {}; + next = jest.fn(); + }); + + it("should use defaultIPSelector when no custom selector is provided", async () => { + const middleware = ipinfoPlus({ token: mockToken }); + + await middleware(mockReq, mockRes, next); + + expect(mockLookupIp).toHaveBeenCalledWith("1.2.3.4"); + expect(mockReq.ipinfo).toEqual({ + ip: "1.2.3.4", + city: "New York", + country: "US", + hostname: "example.com", + org: "Example Org" + }); + expect(next).toHaveBeenCalled(); + }); + + it("should use originatingIPSelector when specified", async () => { + mockLookupIp.mockResolvedValue({ + ip: "5.6.7.8", + city: "San Francisco", + country: "US", + hostname: "proxy.example.com", + org: "Proxy Org" + }); + + const middleware = ipinfoPlus({ + token: mockToken, + ipSelector: originatingIPSelector + }); + + await middleware(mockReq, mockRes, next); + + expect(mockLookupIp).toHaveBeenCalledWith("5.6.7.8"); + expect(mockReq.ipinfo?.ip).toBe("5.6.7.8"); + }); + + it("should use custom ipSelector function when provided", async () => { + const customSelector = jest.fn().mockReturnValue("9.10.11.12"); + + const middleware = ipinfoPlus({ + token: mockToken, + ipSelector: customSelector + }); + + await middleware(mockReq, mockRes, next); + + expect(customSelector).toHaveBeenCalledWith(mockReq); + expect(mockLookupIp).toHaveBeenCalledWith("9.10.11.12"); + }); + + it("should throw IPinfo API errors", async () => { + const errorMessage = "API rate limit exceeded"; + mockLookupIp.mockRejectedValueOnce(new Error(errorMessage)); + const middleware = ipinfoPlus({ token: mockToken }); + + await expect(middleware(mockReq, mockRes, next)).rejects.toThrow( + errorMessage + ); + + expect(mockReq.ipinfo).toBeUndefined(); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/src/index.ts b/src/index.ts index d464222..3d876a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,17 @@ import { IPinfoWrapper, IPinfo, IPinfoLiteWrapper, - IPinfoCoreWrapper + IPinfoCoreWrapper, + IPinfoPlusWrapper } from "node-ipinfo"; import defaultIPSelector from "./ip-selector/default-ip-selector"; import originatingIPSelector from "./ip-selector/originating-ip-selector"; -import { IPinfoLite, IPinfoCore, IPBogon } from "node-ipinfo/dist/src/common"; +import { + IPinfoLite, + IPinfoCore, + IPinfoPlus, + IPBogon +} from "node-ipinfo/dist/src/common"; type MiddlewareOptions = { token?: string; @@ -75,10 +81,31 @@ const ipinfoCoreMiddleware = ({ }; }; +const ipinfoPlusMiddleware = ({ + token = "", + cache, + timeout, + ipSelector +}: MiddlewareOptions = {}) => { + const ipinfo = new IPinfoPlusWrapper(token, cache, timeout); + if (ipSelector == null || typeof ipSelector !== "function") { + ipSelector = defaultIPSelector; + } + return async (req: any, _: any, next: any) => { + const ip = ipSelector?.(req) ?? defaultIPSelector(req); + if (ip) { + const ipInfo: IPinfoPlus | IPBogon = await ipinfo.lookupIp(ip); + req.ipinfo = ipInfo; + } + next(); + }; +}; + export default ipinfoMiddleware; export { defaultIPSelector, originatingIPSelector, ipinfoLiteMiddleware as ipinfoLite, - ipinfoCoreMiddleware as ipinfoCore + ipinfoCoreMiddleware as ipinfoCore, + ipinfoPlusMiddleware as ipinfoPlus };