Skip to content

Commit 063a2c7

Browse files
authored
Merge pull request #15 from ipinfo/silvano/eng-295-add-lite-api-support-to-ipinfonode-express
Add support for Lite API
2 parents 75fa8a6 + e0a30af commit 063a2c7

File tree

9 files changed

+5663
-416
lines changed

9 files changed

+5663
-416
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
version: [20, 22, "latest"]
13+
14+
steps:
15+
- uses: actions/checkout@v3
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: ${{ matrix.version }}
20+
21+
- run: npm ci
22+
23+
- run: npm test

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr
1515

1616
The free plan is limited to 50,000 requests per month, and doesn't include some of the data fields such as IP type and company data. To enable all the data fields and additional request volumes see [https://ipinfo.io/pricing](https://ipinfo.io/pricing)
1717

18-
⚠️ Note: This library does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request.
18+
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.
1919

2020
### Installation
2121

@@ -150,6 +150,24 @@ app.use(ipinfo({
150150
}))
151151
```
152152

153+
### Lite API
154+
155+
The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.
156+
157+
The IP details returned are slightly different from the Core API middleware, though the arguments are identical.
158+
159+
```typescript
160+
const { ipinfoLite } = require('ipinfo-express')
161+
162+
ipinfoLite({
163+
token: "<token>",
164+
cache: <cache_class>,
165+
timeout: 5000,
166+
ipSelector: null
167+
});
168+
```
169+
170+
153171
### Other Libraries
154172

155173
There are official IPinfo client libraries available for many languages including PHP, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. There are also many third-party libraries and integrations available for our API.
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 { ipinfoLite, originatingIPSelector } from "../src/index";
3+
import { IPinfoLite } from "node-ipinfo/dist/src/common";
4+
5+
// Mock the node-ipinfo module
6+
const mockLookupIp = jest.fn();
7+
jest.mock("node-ipinfo", () => ({
8+
IPinfoLiteWrapper: jest.fn().mockImplementation(() => ({
9+
lookupIp: mockLookupIp
10+
}))
11+
}));
12+
13+
describe("ipinfoLiteMiddleware", () => {
14+
const mockToken = "test_token";
15+
let mockReq: Partial<Request> & { ipinfo?: IPinfoLite };
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 = ipinfoLite({ 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 = ipinfoLite({
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 = ipinfoLite({
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 = ipinfoLite({ 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+
});
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 ipinfo, { originatingIPSelector } from "../src/index";
3+
import { IPinfo } from "node-ipinfo";
4+
5+
// Mock the node-ipinfo module
6+
const mockLookupIp = jest.fn();
7+
jest.mock("node-ipinfo", () => ({
8+
IPinfoWrapper: jest.fn().mockImplementation(() => ({
9+
lookupIp: mockLookupIp
10+
}))
11+
}));
12+
13+
describe("ipinfoMiddleware", () => {
14+
const mockToken = "test_token";
15+
let mockReq: Partial<Request> & { ipinfo?: IPinfo };
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 = ipinfo({ 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 = ipinfo({
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 = ipinfo({
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 = ipinfo({ 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+
});

0 commit comments

Comments
 (0)