Skip to content

Commit 176f76e

Browse files
committed
Add support for Plus bundle
1 parent dcfb2ba commit 176f76e

File tree

2 files changed

+145
-3
lines changed

2 files changed

+145
-3
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Request, Response, NextFunction } from "express";
2+
import { ipinfoPlus, originatingIPSelector } from "../src/index";
3+
import { IPinfoPlus } from "node-ipinfo/dist/src/common";
4+
5+
// Mock the node-ipinfo module
6+
const mockLookupIp = jest.fn();
7+
jest.mock("node-ipinfo", () => ({
8+
IPinfoPlusWrapper: jest.fn().mockImplementation(() => ({
9+
lookupIp: mockLookupIp
10+
}))
11+
}));
12+
13+
describe("ipinfoPlusMiddleware", () => {
14+
const mockToken = "test_token";
15+
let mockReq: Partial<Request> & { ipinfo?: IPinfoPlus };
16+
let mockRes: Partial<Response>;
17+
let next: NextFunction;
18+
19+
beforeEach(() => {
20+
// Reset mocks before each test
21+
jest.clearAllMocks();
22+
23+
// Set up default mock response
24+
mockLookupIp.mockResolvedValue({
25+
ip: "1.2.3.4",
26+
city: "New York",
27+
country: "US",
28+
hostname: "example.com",
29+
org: "Example Org"
30+
});
31+
32+
// Setup mock request/response
33+
mockReq = {
34+
ip: "1.2.3.4",
35+
headers: { "x-forwarded-for": "5.6.7.8, 10.0.0.1" },
36+
header: jest.fn((name: string) => {
37+
if (name.toLowerCase() === "set-cookie") {
38+
return ["mock-cookie-1", "mock-cookie-2"];
39+
}
40+
if (name.toLowerCase() === "x-forwarded-for") {
41+
return "5.6.7.8, 10.0.0.1";
42+
}
43+
return undefined;
44+
}) as jest.MockedFunction<
45+
((name: "set-cookie") => string[] | undefined) &
46+
((name: string) => string | undefined)
47+
>
48+
};
49+
mockRes = {};
50+
next = jest.fn();
51+
});
52+
53+
it("should use defaultIPSelector when no custom selector is provided", async () => {
54+
const middleware = ipinfoPlus({ token: mockToken });
55+
56+
await middleware(mockReq, mockRes, next);
57+
58+
expect(mockLookupIp).toHaveBeenCalledWith("1.2.3.4");
59+
expect(mockReq.ipinfo).toEqual({
60+
ip: "1.2.3.4",
61+
city: "New York",
62+
country: "US",
63+
hostname: "example.com",
64+
org: "Example Org"
65+
});
66+
expect(next).toHaveBeenCalled();
67+
});
68+
69+
it("should use originatingIPSelector when specified", async () => {
70+
mockLookupIp.mockResolvedValue({
71+
ip: "5.6.7.8",
72+
city: "San Francisco",
73+
country: "US",
74+
hostname: "proxy.example.com",
75+
org: "Proxy Org"
76+
});
77+
78+
const middleware = ipinfoPlus({
79+
token: mockToken,
80+
ipSelector: originatingIPSelector
81+
});
82+
83+
await middleware(mockReq, mockRes, next);
84+
85+
expect(mockLookupIp).toHaveBeenCalledWith("5.6.7.8");
86+
expect(mockReq.ipinfo?.ip).toBe("5.6.7.8");
87+
});
88+
89+
it("should use custom ipSelector function when provided", async () => {
90+
const customSelector = jest.fn().mockReturnValue("9.10.11.12");
91+
92+
const middleware = ipinfoPlus({
93+
token: mockToken,
94+
ipSelector: customSelector
95+
});
96+
97+
await middleware(mockReq, mockRes, next);
98+
99+
expect(customSelector).toHaveBeenCalledWith(mockReq);
100+
expect(mockLookupIp).toHaveBeenCalledWith("9.10.11.12");
101+
});
102+
103+
it("should throw IPinfo API errors", async () => {
104+
const errorMessage = "API rate limit exceeded";
105+
mockLookupIp.mockRejectedValueOnce(new Error(errorMessage));
106+
const middleware = ipinfoPlus({ token: mockToken });
107+
108+
await expect(middleware(mockReq, mockRes, next)).rejects.toThrow(
109+
errorMessage
110+
);
111+
112+
expect(mockReq.ipinfo).toBeUndefined();
113+
expect(next).not.toHaveBeenCalled();
114+
});
115+
});

src/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import {
22
IPinfoWrapper,
33
IPinfo,
44
IPinfoLiteWrapper,
5-
IPinfoCoreWrapper
5+
IPinfoCoreWrapper,
6+
IPinfoPlusWrapper
67
} from "node-ipinfo";
78
import defaultIPSelector from "./ip-selector/default-ip-selector";
89
import originatingIPSelector from "./ip-selector/originating-ip-selector";
9-
import { IPinfoLite, IPinfoCore, IPBogon } from "node-ipinfo/dist/src/common";
10+
import {
11+
IPinfoLite,
12+
IPinfoCore,
13+
IPinfoPlus,
14+
IPBogon
15+
} from "node-ipinfo/dist/src/common";
1016

1117
type MiddlewareOptions = {
1218
token?: string;
@@ -75,10 +81,31 @@ const ipinfoCoreMiddleware = ({
7581
};
7682
};
7783

84+
const ipinfoPlusMiddleware = ({
85+
token = "",
86+
cache,
87+
timeout,
88+
ipSelector
89+
}: MiddlewareOptions = {}) => {
90+
const ipinfo = new IPinfoPlusWrapper(token, cache, timeout);
91+
if (ipSelector == null || typeof ipSelector !== "function") {
92+
ipSelector = defaultIPSelector;
93+
}
94+
return async (req: any, _: any, next: any) => {
95+
const ip = ipSelector?.(req) ?? defaultIPSelector(req);
96+
if (ip) {
97+
const ipInfo: IPinfoPlus | IPBogon = await ipinfo.lookupIp(ip);
98+
req.ipinfo = ipInfo;
99+
}
100+
next();
101+
};
102+
};
103+
78104
export default ipinfoMiddleware;
79105
export {
80106
defaultIPSelector,
81107
originatingIPSelector,
82108
ipinfoLiteMiddleware as ipinfoLite,
83-
ipinfoCoreMiddleware as ipinfoCore
109+
ipinfoCoreMiddleware as ipinfoCore,
110+
ipinfoPlusMiddleware as ipinfoPlus
84111
};

0 commit comments

Comments
 (0)