Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
push:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version: [20, 22, "latest"]

steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}

- run: npm ci

- run: npm test
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr

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)

⚠️ 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.
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.

### Installation

Expand Down Expand Up @@ -150,6 +150,24 @@ app.use(ipinfo({
}))
```

### Lite API

The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.

The IP details returned are slightly different from the Core API middleware, though the arguments are identical.

```typescript
const { ipinfoLite } = require('ipinfo-express')

ipinfoLite({
token: "<token>",
cache: <cache_class>,
timeout: 5000,
ipSelector: null
});
```


### Other Libraries

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.
Expand Down
115 changes: 115 additions & 0 deletions __tests__/ipinfo-lite-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Request, Response, NextFunction } from "express";
import { ipinfoLite, originatingIPSelector } from "../src/index";
import { IPinfoLite } from "node-ipinfo/dist/src/common";

// Mock the node-ipinfo module
const mockLookupIp = jest.fn();
jest.mock("node-ipinfo", () => ({
IPinfoLiteWrapper: jest.fn().mockImplementation(() => ({
lookupIp: mockLookupIp
}))
}));

describe("ipinfoLiteMiddleware", () => {
const mockToken = "test_token";
let mockReq: Partial<Request> & { ipinfo?: IPinfoLite };
let mockRes: Partial<Response>;
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 = ipinfoLite({ 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 = ipinfoLite({
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 = ipinfoLite({
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 = ipinfoLite({ token: mockToken });

await expect(middleware(mockReq, mockRes, next)).rejects.toThrow(
errorMessage
);

expect(mockReq.ipinfo).toBeUndefined();
expect(next).not.toHaveBeenCalled();
});
});
115 changes: 115 additions & 0 deletions __tests__/ipinfo-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Request, Response, NextFunction } from "express";
import ipinfo, { originatingIPSelector } from "../src/index";
import { IPinfo } from "node-ipinfo";

// Mock the node-ipinfo module
const mockLookupIp = jest.fn();
jest.mock("node-ipinfo", () => ({
IPinfoWrapper: jest.fn().mockImplementation(() => ({
lookupIp: mockLookupIp
}))
}));

describe("ipinfoMiddleware", () => {
const mockToken = "test_token";
let mockReq: Partial<Request> & { ipinfo?: IPinfo };
let mockRes: Partial<Response>;
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 = ipinfo({ 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 = ipinfo({
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 = ipinfo({
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 = ipinfo({ token: mockToken });

await expect(middleware(mockReq, mockRes, next)).rejects.toThrow(
errorMessage
);

expect(mockReq.ipinfo).toBeUndefined();
expect(next).not.toHaveBeenCalled();
});
});
Loading