diff --git a/__tests__/ipinfo-resproxy-middleware.test.ts b/__tests__/ipinfo-resproxy-middleware.test.ts new file mode 100644 index 0000000..9dfc87f --- /dev/null +++ b/__tests__/ipinfo-resproxy-middleware.test.ts @@ -0,0 +1,125 @@ +import { Request, Response, NextFunction } from "express"; +import { ipinfoResproxy, originatingIPSelector } from "../src/index"; +import { Resproxy } from "node-ipinfo/dist/src/common"; + +// Mock the node-ipinfo module +const mockLookupResproxy = jest.fn(); +jest.mock("node-ipinfo", () => ({ + IPinfoWrapper: jest.fn().mockImplementation(() => ({ + lookupResproxy: mockLookupResproxy + })) +})); + +describe("ipinfoResproxyMiddleware", () => { + const mockToken = "test_token"; + let mockReq: Partial & { ipinfo_resproxy?: Resproxy }; + let mockRes: Partial; + let next: NextFunction; + + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + + // Set up default mock response + mockLookupResproxy.mockResolvedValue({ + ip: "175.107.211.204", + last_seen: "2026-01-15", + percent_days_seen: 100, + service: "test_service" + }); + + // Setup mock request/response + mockReq = { + ip: "175.107.211.204", + 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 = ipinfoResproxy({ token: mockToken }); + + await middleware(mockReq, mockRes, next); + + expect(mockLookupResproxy).toHaveBeenCalledWith("175.107.211.204"); + expect(mockReq.ipinfo_resproxy).toEqual({ + ip: "175.107.211.204", + last_seen: "2026-01-15", + percent_days_seen: 100, + service: "test_service" + }); + expect(next).toHaveBeenCalled(); + }); + + it("should use originatingIPSelector when specified", async () => { + mockLookupResproxy.mockResolvedValue({ + ip: "5.6.7.8", + last_seen: "2026-01-15", + percent_days_seen: 50, + service: "proxy_service" + }); + + const middleware = ipinfoResproxy({ + token: mockToken, + ipSelector: originatingIPSelector + }); + + await middleware(mockReq, mockRes, next); + + expect(mockLookupResproxy).toHaveBeenCalledWith("5.6.7.8"); + expect(mockReq.ipinfo_resproxy?.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 = ipinfoResproxy({ + token: mockToken, + ipSelector: customSelector + }); + + await middleware(mockReq, mockRes, next); + + expect(customSelector).toHaveBeenCalledWith(mockReq); + expect(mockLookupResproxy).toHaveBeenCalledWith("9.10.11.12"); + }); + + it("should throw IPinfo API errors", async () => { + const errorMessage = "API rate limit exceeded"; + mockLookupResproxy.mockRejectedValueOnce(new Error(errorMessage)); + const middleware = ipinfoResproxy({ token: mockToken }); + + await expect(middleware(mockReq, mockRes, next)).rejects.toThrow( + errorMessage + ); + + expect(mockReq.ipinfo_resproxy).toBeUndefined(); + expect(next).not.toHaveBeenCalled(); + }); + + it("should pass through empty response when IP not in resproxy database", async () => { + // Empty object simulates IP not in resproxy database + mockLookupResproxy.mockResolvedValue({}); + + const middleware = ipinfoResproxy({ token: mockToken }); + + await middleware(mockReq, mockRes, next); + + expect(mockLookupResproxy).toHaveBeenCalledWith("175.107.211.204"); + expect(mockReq.ipinfo_resproxy).toEqual({}); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/package-lock.json b/package-lock.json index a03d5fd..2f28705 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.0", "license": "Apache-2.0", "dependencies": { - "node-ipinfo": "^4.2.0" + "node-ipinfo": "^4.3.0" }, "devDependencies": { "@types/express": "^4.17.23", @@ -73,7 +73,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1320,7 +1319,6 @@ "integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1933,7 +1931,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3197,7 +3194,6 @@ "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.0.5", "@jest/types": "30.0.5", @@ -4116,9 +4112,9 @@ "license": "MIT" }, "node_modules/node-ipinfo": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/node-ipinfo/-/node-ipinfo-4.2.0.tgz", - "integrity": "sha512-B/tMcJl3MLSWqI9dxENkHH1yb9MFavByDuT1F/USE3Vyv21UTsr8QZ7hF2vte0G1OlS2uzIPNlI3f+66iM8sGg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-ipinfo/-/node-ipinfo-4.3.0.tgz", + "integrity": "sha512-n3ZZXizztFwodSedFeCM8bcpwI/zirxvTs4kbCBoIXE5VxxiXW1lPiJF4JbuskRAYRJpFP24Y+ySee0Z8m5YtA==", "license": "Apache-2.0", "dependencies": { "lru-cache": "^7.18.3", @@ -5094,7 +5090,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5170,7 +5165,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7937a4d..885db8d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ } }, "dependencies": { - "node-ipinfo": "^4.2.0" + "node-ipinfo": "^4.3.0" }, "devDependencies": { "@types/express": "^4.17.23", diff --git a/src/index.ts b/src/index.ts index 3d876a2..992e268 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,8 @@ import { IPinfoLite, IPinfoCore, IPinfoPlus, - IPBogon + IPBogon, + Resproxy } from "node-ipinfo/dist/src/common"; type MiddlewareOptions = { @@ -101,11 +102,32 @@ const ipinfoPlusMiddleware = ({ }; }; +const ipinfoResproxyMiddleware = ({ + token = "", + cache, + timeout, + ipSelector +}: MiddlewareOptions = {}) => { + const ipinfo = new IPinfoWrapper(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 resproxy: Resproxy = await ipinfo.lookupResproxy(ip); + req.ipinfo_resproxy = resproxy; + } + next(); + }; +}; + export default ipinfoMiddleware; export { defaultIPSelector, originatingIPSelector, ipinfoLiteMiddleware as ipinfoLite, ipinfoCoreMiddleware as ipinfoCore, - ipinfoPlusMiddleware as ipinfoPlus + ipinfoPlusMiddleware as ipinfoPlus, + ipinfoResproxyMiddleware as ipinfoResproxy };