Skip to content

Commit 29054ce

Browse files
authored
Merge pull request #18 from ConnerTechnology/update_bulkLookup_api
Update bulk lookup API
2 parents 8f54be7 + 1b5b3f2 commit 29054ce

5 files changed

Lines changed: 137 additions & 73 deletions

File tree

.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module.exports = {
2424
},
2525
overrides: [
2626
{
27+
extends: [
28+
'plugin:jest/recommended',
29+
'plugin:jest/style'
30+
],
2731
files: ['**/*.test.js', '**/*.test.ts'],
2832
rules: {
2933
// Import

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"dotenv-cli": "^3.0.0",
4040
"eslint": "^6.5.1",
4141
"eslint-config-airbnb-base": "^14.0.0",
42+
"eslint-plugin-jest": "^23.0.2",
4243
"eslint-config-prettier": "^6.4.0",
4344
"eslint-plugin-import": "^2.18.2",
4445
"eslint-plugin-prettier": "^3.1.1",

src/ipdata.test.ts

Lines changed: 118 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,114 @@
1+
import keyBy from 'lodash/keyBy';
12
import IPData from './ipdata';
23

4+
const TEST_IP = '1.1.1.1';
5+
const DEFAULT_IP_KEY = 'DEFAULT_IP';
6+
37
describe('constructor()', () => {
48
it('should throw an error if an apiKey is not provided', () => {
59
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
610
// @ts-ignore
7-
expect(() => new IPData()).toThrowError('An API key is required.');
11+
expect(() => new IPData()).toThrow('An API key is required.');
812
});
913

1014
it('should set the apiKey', () => {
1115
const ipdata = new IPData(process.env.IPDATA_API_KEY);
1216
expect(ipdata.apiKey).toEqual(process.env.IPDATA_API_KEY);
1317
});
18+
19+
it('should configure the cache by default', () => {
20+
const ipdata = new IPData(process.env.IPDATA_API_KEY);
21+
expect(ipdata.cache.max).toEqual(4096);
22+
expect(ipdata.cache.maxAge).toEqual(1000 * 60 * 60 * 24);
23+
});
24+
25+
it('should configure the cache', () => {
26+
const max = 1;
27+
const maxAge = 1000;
28+
const ipdata = new IPData(process.env.IPDATA_API_KEY, { max, maxAge });
29+
expect(ipdata.cache.max).toEqual(max);
30+
expect(ipdata.cache.maxAge).toEqual(maxAge);
31+
});
1432
});
1533

1634
describe('lookup()', () => {
1735
const ipdata = new IPData(process.env.IPDATA_API_KEY);
18-
const IP = '1.1.1.1';
1936

2037
afterEach(() => {
2138
return ipdata.cache.reset();
2239
});
2340

24-
it('should throw an error if the ip address is invalid', async () => {
25-
const badIP = '1.1.11';
26-
await expect(ipdata.lookup(badIP)).rejects.toThrowError(`${badIP} is an invalid IP address.`);
27-
});
41+
describe('default IP address', () => {
42+
it('should return information', async () => {
43+
const info = await ipdata.lookup();
44+
expect(info).toHaveProperty('ip');
45+
expect(info).toHaveProperty('status');
46+
});
2847

29-
it('should return default information when no ip address is provided', async () => {
30-
const info = await ipdata.lookup();
31-
expect(info).toHaveProperty('ip');
32-
expect(info).toHaveProperty('status');
33-
});
48+
it('should return information when null is provided', async () => {
49+
const info = await ipdata.lookup(null);
50+
expect(info).toHaveProperty('ip');
51+
expect(info).toHaveProperty('status');
52+
});
3453

35-
it('should return default information when null is provided', async () => {
36-
const info = await ipdata.lookup(null);
37-
expect(info).toHaveProperty('ip');
38-
expect(info).toHaveProperty('status');
39-
});
54+
it('should return information when undefined is provided', async () => {
55+
const info = await ipdata.lookup(undefined);
56+
expect(info).toHaveProperty('ip');
57+
expect(info).toHaveProperty('status');
58+
});
4059

41-
it('should return default information when undefined is provided', async () => {
42-
const info = await ipdata.lookup(undefined);
43-
expect(info).toHaveProperty('ip');
44-
expect(info).toHaveProperty('status');
45-
});
60+
it('should cache the information', async () => {
61+
await ipdata.lookup();
62+
const info = ipdata.cache.get(DEFAULT_IP_KEY);
63+
expect(info).toHaveProperty('ip');
64+
expect(info).toHaveProperty('status');
65+
});
4666

47-
it('should cache the ip address info for the default ip address', async () => {
48-
await ipdata.lookup();
49-
const info = ipdata.cache.get('DEFAULT_IP');
50-
expect(info).toHaveProperty('ip');
51-
expect(info).toHaveProperty('status');
67+
it('should not cache the information', async () => {
68+
const customIPData = new IPData(process.env.IPDATA_API_KEY, { maxAge: -1 });
69+
await customIPData.lookup();
70+
const info = ipdata.cache.get(DEFAULT_IP_KEY);
71+
expect(info).toBeUndefined();
72+
});
5273
});
5374

54-
it('should accept an ip address', async () => {
55-
const info = await ipdata.lookup(IP);
56-
expect(info).toHaveProperty('ip', IP);
57-
expect(info).toHaveProperty('status');
58-
});
75+
describe('custom IP address', () => {
76+
it('should throw an error if the ip address is invalid', async () => {
77+
const badIP = '1.1.11';
78+
await expect(ipdata.lookup(badIP)).rejects.toThrow(`${badIP} is an invalid IP address.`);
79+
});
5980

60-
it('should cache the ip address info', async () => {
61-
await ipdata.lookup(IP);
62-
const info = ipdata.cache.get(IP);
63-
expect(info).toHaveProperty('ip', IP);
64-
expect(info).toHaveProperty('status');
81+
it('should return information', async () => {
82+
const info = await ipdata.lookup(TEST_IP);
83+
expect(info).toHaveProperty('ip', TEST_IP);
84+
expect(info).toHaveProperty('status');
85+
});
86+
87+
it('should cache the information', async () => {
88+
await ipdata.lookup(TEST_IP);
89+
const info = ipdata.cache.get(TEST_IP);
90+
expect(info).toHaveProperty('ip', TEST_IP);
91+
expect(info).toHaveProperty('status');
92+
});
93+
94+
it('should not cache the information', async () => {
95+
const customIPData = new IPData(process.env.IPDATA_API_KEY, { maxAge: -1 });
96+
await customIPData.lookup(TEST_IP);
97+
const info = ipdata.cache.get(TEST_IP);
98+
expect(info).toBeUndefined();
99+
});
65100
});
66101

67102
it('should throw an error if a selectField and fields is provided', async () => {
68-
await expect(ipdata.lookup(null, 'field', ['field'])).rejects.toThrowError(
103+
await expect(ipdata.lookup(null, 'field', ['field'])).rejects.toThrow(
69104
'The selectField and fields parameters cannot be used at the same time.',
70105
);
71106
});
72107

73108
describe('selectField', () => {
74109
it('should throw an error for an invlaid field ', async () => {
75110
const field = 'field';
76-
await expect(ipdata.lookup(null, field)).rejects.toThrowError(`${field} is not a valid field.`);
111+
await expect(ipdata.lookup(null, field)).rejects.toThrow(`${field} is not a valid field.`);
77112
});
78113

79114
it('should return a response with only the field', async () => {
@@ -88,15 +123,15 @@ describe('lookup()', () => {
88123
it('should throw an error for an invlaid field ', async () => {
89124
const field = 'field';
90125
const fields = [field];
91-
await expect(ipdata.lookup(null, null, fields)).rejects.toThrowError(`${field} is not a valid field.`);
126+
await expect(ipdata.lookup(null, null, fields)).rejects.toThrow(`${field} is not a valid field.`);
92127
});
93128

94129
it('should return a response with only the field', async () => {
95130
const field1 = 'ip';
96131
const field2 = 'is_eu';
97132
const fields = [field1, field2];
98-
const info = await ipdata.lookup(IP, null, fields);
99-
expect(info).toHaveProperty(field1, IP);
133+
const info = await ipdata.lookup(TEST_IP, null, fields);
134+
expect(info).toHaveProperty(field1, TEST_IP);
100135
expect(info).toHaveProperty(field2, false);
101136
expect(info).toHaveProperty('status');
102137
});
@@ -105,7 +140,7 @@ describe('lookup()', () => {
105140

106141
describe('bulkLookup()', () => {
107142
const ipdata = new IPData(process.env.IPDATA_API_KEY);
108-
const IP1 = '1.1.1.1';
143+
const IP1 = TEST_IP;
109144
const IP2 = '8.8.8.8';
110145

111146
afterEach(() => {
@@ -114,43 +149,67 @@ describe('bulkLookup()', () => {
114149

115150
it('should throw an error if less than 2 ip addresses are provided', async () => {
116151
const ips = [IP1];
117-
await expect(ipdata.bulkLookup(ips)).rejects.toThrowError(
118-
'Bulk Lookup requires more than 1 IP Address in the payload.',
119-
);
152+
await expect(ipdata.bulkLookup(ips)).rejects.toThrow('Bulk Lookup requires more than 1 IP Address in the payload.');
120153
});
121154

122155
it('should throw an error if an ip address is invalid', async () => {
123156
const badIP = '1.1.11';
124157
const ips = [badIP, IP2];
125-
await expect(ipdata.bulkLookup(ips)).rejects.toThrowError(`${badIP} is an invalid IP address.`);
158+
await expect(ipdata.bulkLookup(ips)).rejects.toThrow(`${badIP} is an invalid IP address.`);
126159
});
127160

128161
it('should return info for the ip addresses', async () => {
129-
const info = await ipdata.bulkLookup([IP1, IP2]);
130-
expect(info).toHaveProperty('responses');
131-
expect(info).toHaveProperty('status');
132-
expect(info.responses[0]).toHaveProperty('ip', IP1);
133-
expect(info.responses[1]).toHaveProperty('ip', IP2);
162+
const result = await ipdata.bulkLookup([IP1, IP2]);
163+
const info = keyBy(result, ipInfo => ipInfo.ip);
164+
expect(info[IP1]).toHaveProperty('ip', IP1);
165+
expect(info[IP1]).toHaveProperty('status');
166+
expect(info[IP2]).toHaveProperty('ip', IP2);
167+
expect(info[IP2]).toHaveProperty('status');
168+
});
169+
170+
it('should cache the info', async () => {
171+
await ipdata.bulkLookup([IP1, IP2]);
172+
const ip1Info = ipdata.cache.get(IP1);
173+
const ip2Info = ipdata.cache.get(IP2);
174+
expect(ip1Info).toHaveProperty('ip', IP1);
175+
expect(ip1Info).toHaveProperty('status');
176+
expect(ip2Info).toHaveProperty('ip', IP2);
177+
expect(ip2Info).toHaveProperty('status');
178+
});
179+
180+
it('should use existing cached ips and merge ones that do not exist', async () => {
181+
const IP3 = '1.0.0.1';
182+
await ipdata.lookup(IP1);
183+
expect(ipdata.cache.get(IP1)).not.toBeUndefined();
184+
const result = await ipdata.bulkLookup([IP1, IP2, IP3]);
185+
const info = keyBy(result, ipInfo => ipInfo.ip);
186+
expect(info[IP1]).toHaveProperty('ip', IP1);
187+
expect(info[IP1]).toHaveProperty('status');
188+
expect(info[IP2]).toHaveProperty('ip', IP2);
189+
expect(info[IP2]).toHaveProperty('status');
190+
expect(info[IP3]).toHaveProperty('ip', IP3);
191+
expect(info[IP3]).toHaveProperty('status');
134192
});
135193

136194
describe('fields', () => {
137195
it('should throw an error for an invlaid field ', async () => {
138196
const field = 'field';
139197
const fields = [field];
140-
await expect(ipdata.bulkLookup([IP1, IP2], fields)).rejects.toThrowError(`${field} is not a valid field.`);
198+
await expect(ipdata.bulkLookup([IP1, IP2], fields)).rejects.toThrow(`${field} is not a valid field.`);
141199
});
142200

143201
it('should return a response with only the field', async () => {
144202
const field1 = 'ip';
145203
const field2 = 'is_eu';
146204
const fields = [field1, field2];
147-
const info = await ipdata.bulkLookup([IP1, IP2], fields);
148-
expect(info).toHaveProperty('responses');
149-
expect(info).toHaveProperty('status');
150-
expect(info.responses[0]).toHaveProperty(field1, IP1);
151-
expect(info.responses[0]).toHaveProperty(field2, false);
152-
expect(info.responses[1]).toHaveProperty(field1, IP2);
153-
expect(info.responses[1]).toHaveProperty(field2, false);
205+
const result = await ipdata.bulkLookup([IP1, IP2], fields);
206+
const info = keyBy(result, ipInfo => ipInfo.ip);
207+
expect(info[IP1]).toHaveProperty(field1, IP1);
208+
expect(info[IP1]).toHaveProperty(field2, false);
209+
expect(info[IP1]).toHaveProperty('status');
210+
expect(info[IP2]).toHaveProperty(field1, IP2);
211+
expect(info[IP2]).toHaveProperty(field2, false);
212+
expect(info[IP2]).toHaveProperty('status');
154213
});
155214
});
156215
});

src/ipdata.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,6 @@ export interface LookupResponse {
123123
status: number;
124124
}
125125

126-
export interface BulkLookupResponse {
127-
responses: LookupResponse[];
128-
status: number;
129-
}
130-
131126
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
132127
interface IPDataParams {
133128
'api-key': string;
@@ -180,18 +175,17 @@ export default class IPData {
180175
}
181176

182177
this.cache.set(ip || DEFAULT_IP, data);
178+
return this.cache.get(ip || DEFAULT_IP);
183179
} catch (e) {
184180
const { response } = e as AxiosError;
185181
if (response) {
186182
return { ...response.data, status: response.status };
187183
}
188184
throw e;
189185
}
190-
191-
return this.cache.get(ip || DEFAULT_IP);
192186
}
193187

194-
async bulkLookup(ips: string[], fields?: string[]): Promise<BulkLookupResponse> {
188+
async bulkLookup(ips: string[], fields?: string[]): Promise<LookupResponse[]> {
195189
const params: IPDataParams = { 'api-key': this.apiKey };
196190
const responses: LookupResponse[] = [];
197191
const bulk = [];
@@ -217,17 +211,14 @@ export default class IPData {
217211
}
218212

219213
try {
220-
let result: BulkLookupResponse = { responses, status: 200 };
221-
222214
if (bulk.length > 0) {
223215
const response = await axios.post(urljoin(BASE_URL, 'bulk'), bulk, { params });
224216
response.data.forEach(info => {
225-
this.cache.set(info.ip, info);
217+
this.cache.set(info.ip, { ...info, status: response.status });
218+
responses.push(this.cache.get(info.ip));
226219
});
227-
result = { responses: [...responses, ...response.data], status: response.status };
228220
}
229-
230-
return result;
221+
return responses;
231222
} catch (e) {
232223
const { response } = e as AxiosError;
233224
if (response) {

0 commit comments

Comments
 (0)