@withjoy/limiter 0.1.2 → 0.1.4-test
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/limitd-redis/LICENSE +21 -0
- package/limitd-redis/README.md +183 -0
- package/limitd-redis/docker-compose.yml +11 -0
- package/limitd-redis/index.js +2 -0
- package/limitd-redis/lib/cb.js +45 -0
- package/limitd-redis/lib/client.js +135 -0
- package/limitd-redis/lib/db.js +501 -0
- package/limitd-redis/lib/db_ping.js +106 -0
- package/limitd-redis/lib/put.lua +31 -0
- package/limitd-redis/lib/take.lua +48 -0
- package/limitd-redis/lib/utils.js +116 -0
- package/limitd-redis/lib/validation.js +64 -0
- package/limitd-redis/node_modules/lru-cache/LICENSE +15 -0
- package/limitd-redis/node_modules/lru-cache/README.md +158 -0
- package/limitd-redis/node_modules/lru-cache/index.js +468 -0
- package/limitd-redis/node_modules/lru-cache/package.json +74 -0
- package/limitd-redis/node_modules/ms/index.js +162 -0
- package/limitd-redis/node_modules/ms/license.md +21 -0
- package/limitd-redis/node_modules/ms/package.json +73 -0
- package/limitd-redis/node_modules/ms/readme.md +59 -0
- package/limitd-redis/node_modules/yallist/LICENSE +15 -0
- package/limitd-redis/node_modules/yallist/README.md +204 -0
- package/limitd-redis/node_modules/yallist/iterator.js +7 -0
- package/limitd-redis/node_modules/yallist/package.json +65 -0
- package/limitd-redis/node_modules/yallist/yallist.js +370 -0
- package/limitd-redis/opslevel.yml +6 -0
- package/limitd-redis/package-lock.json +3484 -0
- package/limitd-redis/package.json +31 -0
- package/limitd-redis/test/cb.tests.js +124 -0
- package/limitd-redis/test/client.tests.js +194 -0
- package/limitd-redis/test/db.tests.js +1318 -0
- package/limitd-redis/test/validation.tests.js +124 -0
- package/limiter.js +83 -19
- package/package.json +3 -2
- package/tests/limiter.test.js +27 -27
- package/tests/performTestWithTestPerMinute.js +1 -1
- package/tests/sanityCheck.js +33 -29
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
/* eslint-env node, mocha */
|
|
2
|
+
const ms = require('ms');
|
|
3
|
+
const async = require('async');
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const LimitDB = require('../lib/db');
|
|
6
|
+
const assert = require('chai').assert;
|
|
7
|
+
const {Toxiproxy, Toxic} = require('toxiproxy-node-client');
|
|
8
|
+
const crypto = require('crypto')
|
|
9
|
+
|
|
10
|
+
const buckets = {
|
|
11
|
+
ip: {
|
|
12
|
+
size: 10,
|
|
13
|
+
per_second: 5,
|
|
14
|
+
overrides: {
|
|
15
|
+
'127.0.0.1': {
|
|
16
|
+
per_second: 100
|
|
17
|
+
},
|
|
18
|
+
'local-lan': {
|
|
19
|
+
match: '192\\.168\\.',
|
|
20
|
+
per_second: 50
|
|
21
|
+
},
|
|
22
|
+
'10.0.0.123': {
|
|
23
|
+
until: new Date(Date.now() - ms('24h') - ms('1m')), //yesterday
|
|
24
|
+
per_second: 50
|
|
25
|
+
},
|
|
26
|
+
'10.0.0.124': {
|
|
27
|
+
until: Date.now() - ms('24h') - ms('1m'), //yesterday
|
|
28
|
+
per_second: 50
|
|
29
|
+
},
|
|
30
|
+
'10.0.0.1': {
|
|
31
|
+
size: 1,
|
|
32
|
+
per_hour: 2
|
|
33
|
+
},
|
|
34
|
+
'0.0.0.0': {
|
|
35
|
+
size: 100,
|
|
36
|
+
unlimited: true
|
|
37
|
+
},
|
|
38
|
+
'8.8.8.8': {
|
|
39
|
+
size: 10
|
|
40
|
+
},
|
|
41
|
+
'9.8.7.6': {
|
|
42
|
+
size: 200,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
user: {
|
|
47
|
+
size: 1,
|
|
48
|
+
per_second: 5,
|
|
49
|
+
overrides: {
|
|
50
|
+
'regexp': {
|
|
51
|
+
match: '^regexp',
|
|
52
|
+
size: 10
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
tenant: {
|
|
57
|
+
size: 1,
|
|
58
|
+
per_second: 1
|
|
59
|
+
},
|
|
60
|
+
global: {
|
|
61
|
+
size: 3,
|
|
62
|
+
per_hour: 2,
|
|
63
|
+
overrides: {
|
|
64
|
+
skipit: {
|
|
65
|
+
skip_n_calls: 2,
|
|
66
|
+
size: 3,
|
|
67
|
+
per_hour: 3
|
|
68
|
+
},
|
|
69
|
+
skipOneSize10: {
|
|
70
|
+
skip_n_calls: 1,
|
|
71
|
+
size: 10,
|
|
72
|
+
per_hour: 0
|
|
73
|
+
},
|
|
74
|
+
skipOneSize3: {
|
|
75
|
+
skip_n_calls: 1,
|
|
76
|
+
size: 3,
|
|
77
|
+
per_hour: 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe('LimitDBRedis', () => {
|
|
85
|
+
let db;
|
|
86
|
+
|
|
87
|
+
beforeEach((done) => {
|
|
88
|
+
db = new LimitDB({ uri: 'localhost', buckets, prefix: 'tests:' });
|
|
89
|
+
db.once('error', done);
|
|
90
|
+
db.once('ready', () => {
|
|
91
|
+
db.resetAll(done);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach((done) => {
|
|
96
|
+
|
|
97
|
+
db.close((err) => {
|
|
98
|
+
// Can't close DB if it was never open
|
|
99
|
+
if (err?.message.indexOf('enableOfflineQueue') > 0) {
|
|
100
|
+
err = undefined;
|
|
101
|
+
}
|
|
102
|
+
done(err);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('#constructor', () => {
|
|
107
|
+
it('should throw an when missing redis information', () => {
|
|
108
|
+
assert.throws(() => new LimitDB({}), /Redis connection information must be specified/);
|
|
109
|
+
});
|
|
110
|
+
it('should throw an when missing bucket configuration', () => {
|
|
111
|
+
assert.throws(() => new LimitDB({ uri: 'localhost:test' }), /Buckets must be specified for Limitd/);
|
|
112
|
+
});
|
|
113
|
+
it('should emit error on failure to connect to redis', (done) => {
|
|
114
|
+
let called = false;
|
|
115
|
+
db = new LimitDB({ uri: 'localhost:test', buckets: {} });
|
|
116
|
+
db.on('error', () => {
|
|
117
|
+
if (!called) {
|
|
118
|
+
called = true;
|
|
119
|
+
return done();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('#configurateBucketKey', () => {
|
|
126
|
+
it('should add new bucket to existing configuration', () => {
|
|
127
|
+
db.configurateBucket('test', { size: 5 });
|
|
128
|
+
assert.containsAllKeys(db.buckets, ['ip', 'test']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should replace configuration of existing type', () => {
|
|
132
|
+
db.configurateBucket('ip', { size: 1 });
|
|
133
|
+
assert.equal(db.buckets.ip.size, 1);
|
|
134
|
+
assert.equal(Object.keys(db.buckets.ip.overrides).length, 0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('TAKE', () => {
|
|
139
|
+
it('should fail on validation', (done) => {
|
|
140
|
+
db.take({}, (err) => {
|
|
141
|
+
assert.match(err.message, /type is required/);
|
|
142
|
+
done();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should keep track of a key', (done) => {
|
|
147
|
+
const params = { type: 'ip', key: '21.17.65.41'};
|
|
148
|
+
db.take(params, (err) => {
|
|
149
|
+
if (err) {
|
|
150
|
+
return done(err);
|
|
151
|
+
}
|
|
152
|
+
db.take(params, (err, result) => {
|
|
153
|
+
if (err) {
|
|
154
|
+
return done(err);
|
|
155
|
+
}
|
|
156
|
+
assert.equal(result.conformant, true);
|
|
157
|
+
assert.equal(result.remaining, 8);
|
|
158
|
+
done();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should add a ttl to buckets', (done) => {
|
|
164
|
+
const params = { type: 'ip', key: '211.45.66.1'};
|
|
165
|
+
db.take(params, (err) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
return done(err);
|
|
168
|
+
}
|
|
169
|
+
db.redis.ttl(`${params.type}:${params.key}`, (err, ttl) => {
|
|
170
|
+
if (err) {
|
|
171
|
+
return done(err);
|
|
172
|
+
}
|
|
173
|
+
assert.equal(db.buckets['ip'].ttl, ttl);
|
|
174
|
+
done();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return TRUE with right remaining and reset after filling up the bucket', (done) => {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
db.take({
|
|
182
|
+
type: 'ip',
|
|
183
|
+
key: '5.5.5.5'
|
|
184
|
+
}, (err) => {
|
|
185
|
+
if (err) {
|
|
186
|
+
return done(err);
|
|
187
|
+
}
|
|
188
|
+
db.put({
|
|
189
|
+
type: 'ip',
|
|
190
|
+
key: '5.5.5.5',
|
|
191
|
+
}, (err) => {
|
|
192
|
+
if (err) {
|
|
193
|
+
return done(err);
|
|
194
|
+
}
|
|
195
|
+
db.take({
|
|
196
|
+
type: 'ip',
|
|
197
|
+
key: '5.5.5.5'
|
|
198
|
+
}, (err, result) => {
|
|
199
|
+
if (err) {
|
|
200
|
+
return done(err);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
assert.ok(result.conformant);
|
|
204
|
+
assert.equal(result.remaining, 9);
|
|
205
|
+
assert.closeTo(result.reset, now / 1000, 3);
|
|
206
|
+
assert.equal(result.limit, 10);
|
|
207
|
+
done();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should return TRUE when traffic is conformant', (done) => {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
db.take({
|
|
216
|
+
type: 'ip',
|
|
217
|
+
key: '1.1.1.1'
|
|
218
|
+
}, (err, result) => {
|
|
219
|
+
if (err) return done(err);
|
|
220
|
+
assert.ok(result.conformant);
|
|
221
|
+
assert.equal(result.remaining, 9);
|
|
222
|
+
assert.closeTo(result.reset, now / 1000, 3);
|
|
223
|
+
assert.equal(result.limit, 10);
|
|
224
|
+
done();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return FALSE when requesting more than the size of the bucket', (done) => {
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
db.take({
|
|
231
|
+
type: 'ip',
|
|
232
|
+
key: '2.2.2.2',
|
|
233
|
+
count: 12
|
|
234
|
+
}, (err, result) => {
|
|
235
|
+
if (err) return done(err);
|
|
236
|
+
assert.notOk(result.conformant);
|
|
237
|
+
assert.equal(result.remaining, 10);
|
|
238
|
+
assert.closeTo(result.reset, now / 1000, 3);
|
|
239
|
+
assert.equal(result.limit, 10);
|
|
240
|
+
done();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should return FALSE when traffic is not conformant', (done) => {
|
|
245
|
+
const takeParams = {
|
|
246
|
+
type: 'ip',
|
|
247
|
+
key: '3.3.3.3'
|
|
248
|
+
};
|
|
249
|
+
async.map(_.range(10), (i, done) => {
|
|
250
|
+
db.take(takeParams, done);
|
|
251
|
+
}, (err, responses) => {
|
|
252
|
+
if (err) return done(err);
|
|
253
|
+
assert.ok(responses.every((r) => { return r.conformant; }));
|
|
254
|
+
db.take(takeParams, (err, response) => {
|
|
255
|
+
assert.notOk(response.conformant);
|
|
256
|
+
assert.equal(response.remaining, 0);
|
|
257
|
+
done();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should return TRUE if an override by name allows more', (done) => {
|
|
263
|
+
const takeParams = {
|
|
264
|
+
type: 'ip',
|
|
265
|
+
key: '127.0.0.1'
|
|
266
|
+
};
|
|
267
|
+
async.each(_.range(10), (i, done) => {
|
|
268
|
+
db.take(takeParams, done);
|
|
269
|
+
}, (err) => {
|
|
270
|
+
if (err) return done(err);
|
|
271
|
+
db.take(takeParams, (err, result) => {
|
|
272
|
+
if (err) return done(err);
|
|
273
|
+
assert.ok(result.conformant);
|
|
274
|
+
assert.ok(result.remaining, 89);
|
|
275
|
+
done();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should return TRUE if an override allows more', (done) => {
|
|
281
|
+
const takeParams = {
|
|
282
|
+
type: 'ip',
|
|
283
|
+
key: '192.168.0.1'
|
|
284
|
+
};
|
|
285
|
+
async.each(_.range(10), (i, done) => {
|
|
286
|
+
db.take(takeParams, done);
|
|
287
|
+
}, (err) => {
|
|
288
|
+
if (err) return done(err);
|
|
289
|
+
db.take(takeParams, (err, result) => {
|
|
290
|
+
assert.ok(result.conformant);
|
|
291
|
+
assert.ok(result.remaining, 39);
|
|
292
|
+
done();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('can expire an override', (done) => {
|
|
298
|
+
const takeParams = {
|
|
299
|
+
type: 'ip',
|
|
300
|
+
key: '10.0.0.123'
|
|
301
|
+
};
|
|
302
|
+
async.each(_.range(10), (i, cb) => {
|
|
303
|
+
db.take(takeParams, cb);
|
|
304
|
+
}, (err) => {
|
|
305
|
+
if (err) {
|
|
306
|
+
return done(err);
|
|
307
|
+
}
|
|
308
|
+
db.take(takeParams, (err, response) => {
|
|
309
|
+
assert.notOk(response.conformant);
|
|
310
|
+
done();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('can parse a date and expire and override', (done) => {
|
|
316
|
+
const takeParams = {
|
|
317
|
+
type: 'ip',
|
|
318
|
+
key: '10.0.0.124'
|
|
319
|
+
};
|
|
320
|
+
async.each(_.range(10), (i, cb) => {
|
|
321
|
+
db.take(takeParams, cb);
|
|
322
|
+
}, (err) => {
|
|
323
|
+
if (err) {
|
|
324
|
+
return done(err);
|
|
325
|
+
}
|
|
326
|
+
db.take(takeParams, (err, response) => {
|
|
327
|
+
assert.notOk(response.conformant);
|
|
328
|
+
done();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should use seconds ceiling for next reset', (done) => {
|
|
334
|
+
// it takes ~1790 msec to fill the bucket with this test
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const requests = _.range(9).map(() => {
|
|
337
|
+
return cb => db.take({ type: 'ip', key: '211.123.12.36' }, cb);
|
|
338
|
+
});
|
|
339
|
+
async.series(requests, (err, results) => {
|
|
340
|
+
if (err) return done(err);
|
|
341
|
+
const lastResult = results[results.length -1];
|
|
342
|
+
assert.ok(lastResult.conformant);
|
|
343
|
+
assert.equal(lastResult.remaining, 1);
|
|
344
|
+
assert.closeTo(lastResult.reset, now / 1000, 3);
|
|
345
|
+
assert.equal(lastResult.limit, 10);
|
|
346
|
+
done();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should set reset to UNIX timestamp regardless of period', (done) => {
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
db.take({ type: 'ip', key: '10.0.0.1' }, (err, result) => {
|
|
353
|
+
if (err) { return done(err); }
|
|
354
|
+
assert.ok(result.conformant);
|
|
355
|
+
assert.equal(result.remaining, 0);
|
|
356
|
+
assert.closeTo(result.reset, now / 1000 + 1800, 1);
|
|
357
|
+
assert.equal(result.limit, 1);
|
|
358
|
+
done();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should work for unlimited', (done) => {
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
db.take({ type: 'ip', key: '0.0.0.0' }, (err, response) => {
|
|
365
|
+
if (err) return done(err);
|
|
366
|
+
assert.ok(response.conformant);
|
|
367
|
+
assert.equal(response.remaining, 100);
|
|
368
|
+
assert.closeTo(response.reset, now / 1000, 1);
|
|
369
|
+
assert.equal(response.limit, 100);
|
|
370
|
+
done();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should work with a fixed bucket', (done) => {
|
|
375
|
+
async.map(_.range(10), (i, done) => {
|
|
376
|
+
db.take({ type: 'ip', key: '8.8.8.8' }, done);
|
|
377
|
+
}, (err, results) => {
|
|
378
|
+
if (err) return done(err);
|
|
379
|
+
results.forEach((r, i) => {
|
|
380
|
+
assert.equal(r.remaining + i + 1, 10);
|
|
381
|
+
});
|
|
382
|
+
assert.ok(results.every(r => r.conformant));
|
|
383
|
+
db.take({ type: 'ip', key: '8.8.8.8' }, (err, response) => {
|
|
384
|
+
assert.notOk(response.conformant);
|
|
385
|
+
done();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should work with RegExp', (done) => {
|
|
391
|
+
db.take({ type: 'user', key: 'regexp|test'}, (err, response) => {
|
|
392
|
+
if (err) {
|
|
393
|
+
return done(err);
|
|
394
|
+
}
|
|
395
|
+
assert.ok(response.conformant);
|
|
396
|
+
assert.equal(response.remaining, 9);
|
|
397
|
+
assert.equal(response.limit, 10);
|
|
398
|
+
done();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should work with "all"', (done) => {
|
|
403
|
+
db.take({ type: 'user', key: 'regexp|test', count: 'all'}, (err, response) => {
|
|
404
|
+
if (err) {
|
|
405
|
+
return done(err);
|
|
406
|
+
}
|
|
407
|
+
assert.ok(response.conformant);
|
|
408
|
+
assert.equal(response.remaining, 0);
|
|
409
|
+
assert.equal(response.limit, 10);
|
|
410
|
+
done();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should work with count=0', (done) => {
|
|
415
|
+
db.take({ type: 'ip', key: '9.8.7.6', count: 0 }, (err, response) => {
|
|
416
|
+
if (err) {
|
|
417
|
+
return done(err);
|
|
418
|
+
}
|
|
419
|
+
assert.ok(response.conformant);
|
|
420
|
+
assert.equal(response.remaining, 200);
|
|
421
|
+
assert.equal(response.limit, 200);
|
|
422
|
+
done();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
[
|
|
427
|
+
'0',
|
|
428
|
+
0.5,
|
|
429
|
+
'ALL',
|
|
430
|
+
true,
|
|
431
|
+
1n,
|
|
432
|
+
{},
|
|
433
|
+
].forEach((count) => {
|
|
434
|
+
it(`should not work for non-integer count=${count}`, (done) => {
|
|
435
|
+
const opts = {
|
|
436
|
+
type: 'ip',
|
|
437
|
+
key: '9.8.7.6',
|
|
438
|
+
count,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
assert.throws(() => db.take(opts, () => {}), /if provided, count must be 'all' or an integer value/);
|
|
442
|
+
done();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should use size config override when provided', (done) => {
|
|
447
|
+
const configOverride = { size : 7 };
|
|
448
|
+
db.take({ type: 'ip', key: '7.7.7.7', configOverride}, (err, response) => {
|
|
449
|
+
if (err) {
|
|
450
|
+
return done(err);
|
|
451
|
+
}
|
|
452
|
+
assert.ok(response.conformant);
|
|
453
|
+
assert.equal(response.remaining, 6);
|
|
454
|
+
assert.equal(response.limit, 7);
|
|
455
|
+
done();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should use per interval config override when provided', (done) => {
|
|
460
|
+
const oneDayInMs = ms('24h');
|
|
461
|
+
const configOverride = { per_day: 1 };
|
|
462
|
+
db.take({ type: 'ip', key: '7.7.7.8', configOverride}, (err, response) => {
|
|
463
|
+
if (err) {
|
|
464
|
+
return done(err);
|
|
465
|
+
}
|
|
466
|
+
const dayFromNow = Date.now() + oneDayInMs;
|
|
467
|
+
assert.closeTo(response.reset, dayFromNow / 1000, 3);
|
|
468
|
+
done();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should use size AND interval config override when provided', (done) => {
|
|
473
|
+
const oneDayInMs = ms('24h');
|
|
474
|
+
const configOverride = { size: 3, per_day: 1 };
|
|
475
|
+
db.take({ type: 'ip', key: '7.7.7.8', configOverride}, (err, response) => {
|
|
476
|
+
if (err) {
|
|
477
|
+
return done(err);
|
|
478
|
+
}
|
|
479
|
+
assert.ok(response.conformant);
|
|
480
|
+
assert.equal(response.remaining, 2);
|
|
481
|
+
assert.equal(response.limit, 3);
|
|
482
|
+
|
|
483
|
+
const dayFromNow = Date.now() + oneDayInMs;
|
|
484
|
+
assert.closeTo(response.reset, dayFromNow / 1000, 3);
|
|
485
|
+
done();
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should set ttl to reflect config override', (done) => {
|
|
490
|
+
const configOverride = { per_day: 5 };
|
|
491
|
+
const params = { type: 'ip', key: '7.7.7.9', configOverride};
|
|
492
|
+
db.take(params, (err) => {
|
|
493
|
+
if (err) {
|
|
494
|
+
return done(err);
|
|
495
|
+
}
|
|
496
|
+
db.redis.ttl(`${params.type}:${params.key}`, (err, ttl) => {
|
|
497
|
+
if (err) {
|
|
498
|
+
return done(err);
|
|
499
|
+
}
|
|
500
|
+
assert.equal(ttl, 86400);
|
|
501
|
+
done();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should work with no overrides', (done) => {
|
|
507
|
+
const takeParams = { type: 'tenant', key: 'foo'};
|
|
508
|
+
db.take(takeParams, (err, response) => {
|
|
509
|
+
assert.ok(response.conformant);
|
|
510
|
+
assert.equal(response.remaining, 0);
|
|
511
|
+
done();
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should work with thousands of overrides', (done) => {
|
|
516
|
+
const big = _.cloneDeep(buckets);
|
|
517
|
+
for (let i = 0; i < 10000; i++) {
|
|
518
|
+
big.ip.overrides[`regex${i}`] = {
|
|
519
|
+
match: `172\\.16\\.${i}`,
|
|
520
|
+
per_second: 10
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
db.configurateBuckets(big);
|
|
524
|
+
|
|
525
|
+
const takeParams = { type: 'ip', key: '172.16.1.1'};
|
|
526
|
+
async.map(_.range(10), (i, done) => {
|
|
527
|
+
db.take(takeParams, done);
|
|
528
|
+
}, (err, responses) => {
|
|
529
|
+
if (err) return done(err);
|
|
530
|
+
assert.ok(responses.every((r) => { return r.conformant; }));
|
|
531
|
+
db.take(takeParams, (err, response) => {
|
|
532
|
+
assert.notOk(response.conformant);
|
|
533
|
+
assert.equal(response.remaining, 0);
|
|
534
|
+
done();
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should call redis and not set local cache count', (done) => {
|
|
540
|
+
const params = { type: 'global', key: 'aTenant'};
|
|
541
|
+
db.take(params, (err) => {
|
|
542
|
+
if (err) {
|
|
543
|
+
return done(err);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
assert.equal(db.callCounts['global:aTenant'], undefined);
|
|
547
|
+
done();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('skip calls', () => {
|
|
552
|
+
it('should skip calls', (done) => {
|
|
553
|
+
const params = { type: 'global', key: 'skipit'};
|
|
554
|
+
|
|
555
|
+
async.series([
|
|
556
|
+
(cb) => db.take(params, cb), // redis
|
|
557
|
+
(cb) => db.take(params, cb), // cache
|
|
558
|
+
(cb) => db.take(params, cb), // cache
|
|
559
|
+
(cb) => {
|
|
560
|
+
assert.equal(db.callCounts.get('global:skipit').count, 2);
|
|
561
|
+
cb();
|
|
562
|
+
},
|
|
563
|
+
(cb) => db.take(params, cb), // redis
|
|
564
|
+
(cb) => db.take(params, cb), // cache
|
|
565
|
+
(cb) => db.take(params, cb), // cache
|
|
566
|
+
(cb) => db.take(params, cb), // redis (first nonconformant)
|
|
567
|
+
(cb) => db.take(params, cb), // cache (first cached)
|
|
568
|
+
(cb) => {
|
|
569
|
+
assert.equal(db.callCounts.get('global:skipit').count, 1);
|
|
570
|
+
assert.notOk(db.callCounts.get('global:skipit').res.conformant);
|
|
571
|
+
cb();
|
|
572
|
+
},
|
|
573
|
+
], (err, _results) => {
|
|
574
|
+
if (err) {
|
|
575
|
+
return done(err);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
done();
|
|
579
|
+
})
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('should take correct number of tokens for skipped calls with single count', (done) => {
|
|
583
|
+
const params = { type: 'global', key: 'skipOneSize3'};
|
|
584
|
+
|
|
585
|
+
// size = 3
|
|
586
|
+
// skip_n_calls = 1
|
|
587
|
+
// no refill
|
|
588
|
+
async.series([
|
|
589
|
+
(cb) => db.get(params, (_, {remaining}) => { assert.equal(remaining, 3); cb(); }),
|
|
590
|
+
|
|
591
|
+
// call 1 - redis
|
|
592
|
+
// takes 1 token
|
|
593
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
594
|
+
assert.equal(remaining, 2);
|
|
595
|
+
assert.ok(conformant)
|
|
596
|
+
cb();
|
|
597
|
+
}),
|
|
598
|
+
|
|
599
|
+
// call 2 - skipped
|
|
600
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
601
|
+
assert.equal(remaining, 2);
|
|
602
|
+
assert.ok(conformant)
|
|
603
|
+
cb();
|
|
604
|
+
}),
|
|
605
|
+
|
|
606
|
+
// call 3 - redis
|
|
607
|
+
// takes 2 tokens here, 1 for current call and one for previously skipped call
|
|
608
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
609
|
+
assert.equal(remaining, 0);
|
|
610
|
+
assert.ok(conformant)
|
|
611
|
+
cb();
|
|
612
|
+
}),
|
|
613
|
+
|
|
614
|
+
// call 4 - skipped
|
|
615
|
+
// Note: this is the margin of error introduced by skip_n_calls. Without skip_n_calls, this call would be
|
|
616
|
+
// non-conformant.
|
|
617
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
618
|
+
assert.equal(remaining, 0);
|
|
619
|
+
assert.ok(conformant);
|
|
620
|
+
cb();
|
|
621
|
+
}),
|
|
622
|
+
|
|
623
|
+
// call 5 - redis
|
|
624
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
625
|
+
assert.equal(remaining, 0);
|
|
626
|
+
assert.notOk(conformant);
|
|
627
|
+
cb();
|
|
628
|
+
}),
|
|
629
|
+
], (err, _results) => {
|
|
630
|
+
if (err) {
|
|
631
|
+
return done(err);
|
|
632
|
+
}
|
|
633
|
+
done();
|
|
634
|
+
})
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should take correct number of tokens for skipped calls with multi count', (done) => {
|
|
638
|
+
const params = { type: 'global', key: 'skipOneSize10', count: 2};
|
|
639
|
+
|
|
640
|
+
// size = 10
|
|
641
|
+
// skip_n_calls = 1
|
|
642
|
+
// no refill
|
|
643
|
+
async.series([
|
|
644
|
+
(cb) => db.get(params, (_, {remaining}) => { assert.equal(remaining, 10); cb(); }),
|
|
645
|
+
|
|
646
|
+
// call 1 - redis
|
|
647
|
+
// takes 2 tokens
|
|
648
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
649
|
+
assert.equal(remaining, 8);
|
|
650
|
+
assert.ok(conformant)
|
|
651
|
+
cb();
|
|
652
|
+
}),
|
|
653
|
+
|
|
654
|
+
// call 2 - skipped
|
|
655
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
656
|
+
assert.equal(remaining, 8);
|
|
657
|
+
assert.ok(conformant)
|
|
658
|
+
cb();
|
|
659
|
+
}),
|
|
660
|
+
|
|
661
|
+
// call 3 - redis
|
|
662
|
+
// takes 4 tokens here, 2 for current call and 2 for previously skipped call
|
|
663
|
+
(cb) => db.take(params, (_, { remaining, conformant }) => {
|
|
664
|
+
assert.equal(remaining, 4);
|
|
665
|
+
assert.ok(conformant)
|
|
666
|
+
cb();
|
|
667
|
+
}),
|
|
668
|
+
], (err, _results) => {
|
|
669
|
+
if (err) {
|
|
670
|
+
return done(err);
|
|
671
|
+
}
|
|
672
|
+
done();
|
|
673
|
+
})
|
|
674
|
+
});
|
|
675
|
+
})
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
describe('PUT', () => {
|
|
679
|
+
it('should fail on validation', (done) => {
|
|
680
|
+
db.put({}, (err) => {
|
|
681
|
+
assert.match(err.message, /type is required/);
|
|
682
|
+
done();
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should add to the bucket', (done) => {
|
|
687
|
+
db.take({ type: 'ip', key: '8.8.8.8', count: 5 }, (err) => {
|
|
688
|
+
if (err) {
|
|
689
|
+
return done(err);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
db.put({ type: 'ip', key: '8.8.8.8', count: 4 }, (err, result) => {
|
|
693
|
+
if (err) {
|
|
694
|
+
return done(err);
|
|
695
|
+
}
|
|
696
|
+
assert.equal(result.remaining, 9);
|
|
697
|
+
done();
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should do nothing if bucket is already full', (done) => {
|
|
703
|
+
const key = '1.2.3.4';
|
|
704
|
+
db.put({ type: 'ip', key, count: 1 }, (err, result) => {
|
|
705
|
+
if (err) {
|
|
706
|
+
return done(err);
|
|
707
|
+
}
|
|
708
|
+
assert.equal(result.remaining, 10);
|
|
709
|
+
|
|
710
|
+
db.take({ type: 'ip', key, count: 1 }, (err, result) => {
|
|
711
|
+
if (err) {
|
|
712
|
+
return done(err);
|
|
713
|
+
}
|
|
714
|
+
assert.equal(result.remaining, 9);
|
|
715
|
+
done();
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('should not put more than the bucket size', (done) => {
|
|
721
|
+
db.take({ type: 'ip', key: '8.8.8.8', count: 2 }, (err) => {
|
|
722
|
+
if (err) {
|
|
723
|
+
return done(err);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
db.put({ type: 'ip', key: '8.8.8.8', count: 4 }, (err, result) => {
|
|
727
|
+
if (err) {
|
|
728
|
+
return done(err);
|
|
729
|
+
}
|
|
730
|
+
assert.equal(result.remaining, 10);
|
|
731
|
+
done();
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('should not override on unlimited buckets', (done) => {
|
|
737
|
+
const bucketKey = { type: 'ip', key: '0.0.0.0', count: 1000 };
|
|
738
|
+
db.put(bucketKey, (err, result) => {
|
|
739
|
+
if (err) {
|
|
740
|
+
return done(err);
|
|
741
|
+
}
|
|
742
|
+
assert.equal(result.remaining, 100);
|
|
743
|
+
done();
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should restore the bucket when reseting', (done) => {
|
|
748
|
+
const bucketKey = { type: 'ip', key: '211.123.12.12' };
|
|
749
|
+
db.take(Object.assign({ count: 'all' }, bucketKey), (err) => {
|
|
750
|
+
if (err) return done(err);
|
|
751
|
+
db.put(bucketKey, (err) => {
|
|
752
|
+
if (err) return done(err);
|
|
753
|
+
db.take(bucketKey, (err, response) => {
|
|
754
|
+
if (err) return done(err);
|
|
755
|
+
assert.equal(response.remaining, 9);
|
|
756
|
+
done();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should restore the bucket when reseting with all', (done) => {
|
|
763
|
+
const takeParams = { type: 'ip', key: '21.17.65.41', count: 9 };
|
|
764
|
+
db.take(takeParams, (err) => {
|
|
765
|
+
if (err) return done(err);
|
|
766
|
+
db.put({ type: 'ip', key: '21.17.65.41', count: 'all' }, (err) => {
|
|
767
|
+
if (err) return done(err);
|
|
768
|
+
db.take(takeParams, (err, response) => {
|
|
769
|
+
if (err) return done(err);
|
|
770
|
+
assert.equal(response.conformant, true);
|
|
771
|
+
assert.equal(response.remaining, 1);
|
|
772
|
+
done();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should restore nothing when count=0', (done) => {
|
|
779
|
+
db.take({ type: 'ip', key: '9.8.7.6', count: 123 }, (err) => {
|
|
780
|
+
if (err) return done(err);
|
|
781
|
+
db.put({ type: 'ip', key: '9.8.7.6', count: 0 }, (err) => {
|
|
782
|
+
if (err) return done(err);
|
|
783
|
+
db.take({ type: 'ip', key: '9.8.7.6', count: 0 }, (err, response) => {
|
|
784
|
+
if (err) return done(err);
|
|
785
|
+
assert.equal(response.conformant, true);
|
|
786
|
+
assert.equal(response.remaining, 77);
|
|
787
|
+
done();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
[
|
|
794
|
+
'0',
|
|
795
|
+
0.5,
|
|
796
|
+
'ALL',
|
|
797
|
+
true,
|
|
798
|
+
1n,
|
|
799
|
+
{},
|
|
800
|
+
].forEach((count) => {
|
|
801
|
+
it(`should not work for non-integer count=${count}`, (done) => {
|
|
802
|
+
const opts = {
|
|
803
|
+
type: 'ip',
|
|
804
|
+
key: '9.8.7.6',
|
|
805
|
+
count,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
assert.throws(() => db.put(opts, () => {}), /if provided, count must be 'all' or an integer value/);
|
|
809
|
+
done();
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('should be able to reset without callback', (done) => {
|
|
814
|
+
const bucketKey = { type: 'ip', key: '211.123.12.12'};
|
|
815
|
+
db.take(bucketKey, (err) => {
|
|
816
|
+
if (err) return done(err);
|
|
817
|
+
db.put(bucketKey);
|
|
818
|
+
setImmediate(() => {
|
|
819
|
+
db.take(bucketKey, (err, response) => {
|
|
820
|
+
if (err) return done(err);
|
|
821
|
+
assert.equal(response.remaining, 9);
|
|
822
|
+
done();
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should work for a fixed bucket', (done) => {
|
|
829
|
+
db.take({ type: 'ip', key: '8.8.8.8' }, (err, result) => {
|
|
830
|
+
assert.ok(result.conformant);
|
|
831
|
+
db.put({ type: 'ip', key: '8.8.8.8' }, (err, result) => {
|
|
832
|
+
if (err) return done(err);
|
|
833
|
+
assert.equal(result.remaining, 10);
|
|
834
|
+
done();
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should work with negative values', (done) => {
|
|
840
|
+
db.put({ type: 'ip', key: '8.8.8.1', count: -100 }, (err, result) => {
|
|
841
|
+
if (err) {
|
|
842
|
+
return done(err);
|
|
843
|
+
}
|
|
844
|
+
assert.closeTo(result.remaining, -90, 1);
|
|
845
|
+
|
|
846
|
+
db.take({ type: 'ip', key: '8.8.8.1' }, (err, result) => {
|
|
847
|
+
if (err) {
|
|
848
|
+
return done(err);
|
|
849
|
+
}
|
|
850
|
+
assert.equal(result.conformant, false);
|
|
851
|
+
assert.closeTo(result.remaining, -89, 1);
|
|
852
|
+
done();
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('should use size config override when provided', (done) => {
|
|
858
|
+
const configOverride = { size: 4 };
|
|
859
|
+
const bucketKey = { type: 'ip', key: '7.7.7.9', configOverride };
|
|
860
|
+
db.take(Object.assign({ count: 'all' }, bucketKey), (err) => {
|
|
861
|
+
if (err) return done(err);
|
|
862
|
+
db.put(bucketKey, (err) => { // restores all 4
|
|
863
|
+
if (err) return done(err);
|
|
864
|
+
db.take(bucketKey, (err, response) => { // takes 1, 3 remain
|
|
865
|
+
if (err) return done(err);
|
|
866
|
+
assert.equal(response.remaining, 3);
|
|
867
|
+
done();
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should use per interval config override when provided', (done) => {
|
|
874
|
+
const oneDayInMs = ms('24h');
|
|
875
|
+
const configOverride = { per_day: 1 };
|
|
876
|
+
const bucketKey = { type: 'ip', key: '7.7.7.10', configOverride };
|
|
877
|
+
db.take(Object.assign({ count: 'all' }, bucketKey), (err) => {
|
|
878
|
+
if (err) return done(err);
|
|
879
|
+
db.put(bucketKey, (err) => { // restores all 4
|
|
880
|
+
if (err) return done(err);
|
|
881
|
+
db.take(bucketKey, (err, response) => { // takes 1, 3 remain
|
|
882
|
+
if (err) return done(err);
|
|
883
|
+
const dayFromNow = Date.now() + oneDayInMs;
|
|
884
|
+
assert.closeTo(response.reset, dayFromNow / 1000, 3);
|
|
885
|
+
done();
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should use size AND per interval config override when provided', (done) => {
|
|
892
|
+
const oneDayInMs = ms('24h');
|
|
893
|
+
const configOverride = { size: 4, per_day: 1 };
|
|
894
|
+
const bucketKey = { type: 'ip', key: '7.7.7.11', configOverride };
|
|
895
|
+
db.take(Object.assign({ count: 'all' }, bucketKey), (err) => {
|
|
896
|
+
if (err) return done(err);
|
|
897
|
+
db.put(bucketKey, (err) => { // restores all 4
|
|
898
|
+
if (err) return done(err);
|
|
899
|
+
db.take(bucketKey, (err, response) => { // takes 1, 3 remain
|
|
900
|
+
if (err) return done(err);
|
|
901
|
+
assert.equal(response.remaining, 3);
|
|
902
|
+
const dayFromNow = Date.now() + oneDayInMs;
|
|
903
|
+
assert.closeTo(response.reset, dayFromNow / 1000, 3);
|
|
904
|
+
done();
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('should set ttl to reflect config override', (done) => {
|
|
911
|
+
const configOverride = { per_day: 5 };
|
|
912
|
+
const bucketKey = { type: 'ip', key: '7.7.7.12', configOverride};
|
|
913
|
+
db.take(Object.assign({ count: 'all' }, bucketKey), (err) => {
|
|
914
|
+
if (err) return done(err);
|
|
915
|
+
db.put(bucketKey, (err) => { // restores all 4
|
|
916
|
+
if (err) return done(err);
|
|
917
|
+
db.take(bucketKey, (err) => { // takes 1, 3 remain
|
|
918
|
+
if (err) return done(err);
|
|
919
|
+
db.redis.ttl(`${bucketKey.type}:${bucketKey.key}`, (err, ttl) => {
|
|
920
|
+
if (err) {
|
|
921
|
+
return done(err);
|
|
922
|
+
}
|
|
923
|
+
assert.equal(ttl, 86400);
|
|
924
|
+
done();
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
describe('GET', () => {
|
|
933
|
+
it('should fail on validation', (done) => {
|
|
934
|
+
db.get({}, (err) => {
|
|
935
|
+
assert.match(err.message, /type is required/);
|
|
936
|
+
done();
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should return the bucket default for remaining when key does not exist', (done) => {
|
|
941
|
+
db.get({type: 'ip', key: '8.8.8.8'}, (err, result) => {
|
|
942
|
+
if (err) {
|
|
943
|
+
return done(err);
|
|
944
|
+
}
|
|
945
|
+
assert.equal(result.remaining, 10);
|
|
946
|
+
done();
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('should retrieve the bucket for an existing key', (done) => {
|
|
951
|
+
db.take({ type: 'ip', key: '8.8.8.8', count: 1 }, (err) => {
|
|
952
|
+
if (err) {
|
|
953
|
+
return done(err);
|
|
954
|
+
}
|
|
955
|
+
db.get({type: 'ip', key: '8.8.8.8'}, (err, result) => {
|
|
956
|
+
if (err) {
|
|
957
|
+
return done(err);
|
|
958
|
+
}
|
|
959
|
+
assert.equal(result.remaining, 9);
|
|
960
|
+
|
|
961
|
+
db.get({type: 'ip', key: '8.8.8.8'}, (err, result) => {
|
|
962
|
+
if (err) {
|
|
963
|
+
return done(err);
|
|
964
|
+
}
|
|
965
|
+
assert.equal(result.remaining, 9);
|
|
966
|
+
done();
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('should return the bucket for an unlimited key', (done) => {
|
|
973
|
+
db.get({type: 'ip', key: '0.0.0.0'}, (err, result) => {
|
|
974
|
+
if (err) {
|
|
975
|
+
return done(err);
|
|
976
|
+
}
|
|
977
|
+
assert.equal(result.remaining, 100);
|
|
978
|
+
|
|
979
|
+
db.take({ type: 'ip', key: '0.0.0.0', count: 1 }, (err) => {
|
|
980
|
+
if (err) {
|
|
981
|
+
return done(err);
|
|
982
|
+
}
|
|
983
|
+
db.get({type: 'ip', key: '0.0.0.0'}, (err, result) => {
|
|
984
|
+
if (err) {
|
|
985
|
+
return done(err);
|
|
986
|
+
}
|
|
987
|
+
assert.equal(result.remaining, 100);
|
|
988
|
+
assert.equal(result.limit, 100);
|
|
989
|
+
assert.exists(result.reset);
|
|
990
|
+
done();
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('should use size config override when provided', (done) => {
|
|
997
|
+
const configOverride = { size: 7 };
|
|
998
|
+
db.get({type: 'ip', key: '7.7.7.13', configOverride}, (err, result) => {
|
|
999
|
+
if (err) {
|
|
1000
|
+
return done(err);
|
|
1001
|
+
}
|
|
1002
|
+
assert.equal(result.remaining, 7);
|
|
1003
|
+
assert.equal(result.limit, 7);
|
|
1004
|
+
done();
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should use per interval config override when provided', (done) => {
|
|
1009
|
+
const oneDayInMs = ms('24h');
|
|
1010
|
+
const configOverride = { per_day: 1 };
|
|
1011
|
+
db.take({ type: 'ip', key: '7.7.7.14', configOverride }, (err) => {
|
|
1012
|
+
if (err) {
|
|
1013
|
+
return done(err);
|
|
1014
|
+
}
|
|
1015
|
+
db.get({ type: 'ip', key: '7.7.7.14', configOverride }, (err, result) => {
|
|
1016
|
+
if (err) {
|
|
1017
|
+
return done(err);
|
|
1018
|
+
}
|
|
1019
|
+
const dayFromNow = Date.now() + oneDayInMs;
|
|
1020
|
+
assert.closeTo(result.reset, dayFromNow / 1000, 3);
|
|
1021
|
+
done();
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
describe('WAIT', () => {
|
|
1028
|
+
it('should work with a simple request', (done) => {
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
db.wait({ type: 'ip', key: '211.76.23.4' }, (err, response) => {
|
|
1031
|
+
if (err) return done(err);
|
|
1032
|
+
assert.ok(response.conformant);
|
|
1033
|
+
assert.notOk(response.delayed);
|
|
1034
|
+
assert.equal(response.remaining, 9);
|
|
1035
|
+
assert.closeTo(response.reset, now / 1000, 3);
|
|
1036
|
+
done();
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should be delayed when traffic is non conformant', (done) => {
|
|
1041
|
+
db.take({
|
|
1042
|
+
type: 'ip',
|
|
1043
|
+
key: '211.76.23.5',
|
|
1044
|
+
count: 10
|
|
1045
|
+
}, (err) => {
|
|
1046
|
+
if (err) return done(err);
|
|
1047
|
+
const waitingSince = Date.now();
|
|
1048
|
+
db.wait({
|
|
1049
|
+
type: 'ip',
|
|
1050
|
+
key: '211.76.23.5',
|
|
1051
|
+
count: 3
|
|
1052
|
+
}, (err, response) => {
|
|
1053
|
+
if (err) { return done(err); }
|
|
1054
|
+
var waited = Date.now() - waitingSince;
|
|
1055
|
+
assert.ok(response.conformant);
|
|
1056
|
+
assert.ok(response.delayed);
|
|
1057
|
+
assert.closeTo(waited, 600, 20);
|
|
1058
|
+
done();
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('should not be delayed when traffic is non conformant and count=0', (done) => {
|
|
1064
|
+
db.take({
|
|
1065
|
+
type: 'ip',
|
|
1066
|
+
key: '211.76.23.5',
|
|
1067
|
+
count: 10
|
|
1068
|
+
}, (err) => {
|
|
1069
|
+
if (err) return done(err);
|
|
1070
|
+
const waitingSince = Date.now();
|
|
1071
|
+
db.wait({
|
|
1072
|
+
type: 'ip',
|
|
1073
|
+
key: '211.76.23.5',
|
|
1074
|
+
count: 0
|
|
1075
|
+
}, (err, response) => {
|
|
1076
|
+
if (err) { return done(err); }
|
|
1077
|
+
var waited = Date.now() - waitingSince;
|
|
1078
|
+
assert.ok(response.conformant);
|
|
1079
|
+
assert.notOk(response.delayed);
|
|
1080
|
+
assert.closeTo(waited, 0, 20);
|
|
1081
|
+
done();
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
it('should use per interval config override when provided', (done) => {
|
|
1088
|
+
const oneSecondInMs = ms('1s') / 3;
|
|
1089
|
+
const configOverride = { per_second: 3, size: 10 };
|
|
1090
|
+
db.take({
|
|
1091
|
+
type: 'ip',
|
|
1092
|
+
key: '211.76.23.6',
|
|
1093
|
+
count: 10,
|
|
1094
|
+
configOverride
|
|
1095
|
+
}, (err) => {
|
|
1096
|
+
if (err) return done(err);
|
|
1097
|
+
const waitingSince = Date.now();
|
|
1098
|
+
db.wait({
|
|
1099
|
+
type: 'ip',
|
|
1100
|
+
key: '211.76.23.6',
|
|
1101
|
+
count: 1,
|
|
1102
|
+
configOverride
|
|
1103
|
+
}, (err, response) => {
|
|
1104
|
+
if (err) { return done(err); }
|
|
1105
|
+
var waited = Date.now() - waitingSince;
|
|
1106
|
+
assert.ok(response.conformant);
|
|
1107
|
+
assert.ok(response.delayed);
|
|
1108
|
+
assert.closeTo(waited, oneSecondInMs, 20);
|
|
1109
|
+
done();
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
describe('#resetAll', () => {
|
|
1116
|
+
it('should reset all keys of all buckets', (done) => {
|
|
1117
|
+
async.parallel([
|
|
1118
|
+
// Empty those buckets...
|
|
1119
|
+
(cb) => db.take({ type: 'ip', key: '1.1.1.1', count: buckets.ip.size }, cb),
|
|
1120
|
+
(cb) => db.take({ type: 'ip', key: '2.2.2.2', count: buckets.ip.size }, cb),
|
|
1121
|
+
(cb) => db.take({ type: 'user', key: 'some_user', count: buckets.user.size }, cb)
|
|
1122
|
+
], (err) => {
|
|
1123
|
+
if (err) {
|
|
1124
|
+
return done(err);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
db.resetAll((err) => {
|
|
1128
|
+
if (err) {
|
|
1129
|
+
return done(err);
|
|
1130
|
+
}
|
|
1131
|
+
async.parallel([
|
|
1132
|
+
(cb) => db.take({ type: 'ip', key: '1.1.1.1' }, cb),
|
|
1133
|
+
(cb) => db.take({ type: 'ip', key: '2.2.2.2' }, cb),
|
|
1134
|
+
(cb) => db.take({ type: 'user', key: 'some_user' }, cb)
|
|
1135
|
+
], (err, results) => {
|
|
1136
|
+
if (err) {
|
|
1137
|
+
return done(err);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
assert.equal(results[0].remaining, buckets.ip.size - 1);
|
|
1141
|
+
assert.equal(results[0].conformant, true);
|
|
1142
|
+
assert.equal(results[1].remaining, buckets.ip.size - 1);
|
|
1143
|
+
assert.equal(results[0].conformant, true);
|
|
1144
|
+
assert.equal(results[2].remaining, buckets.user.size - 1);
|
|
1145
|
+
assert.equal(results[2].conformant, true);
|
|
1146
|
+
done();
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
describe('LimitDBRedis Ping', () => {
|
|
1155
|
+
let ping = {
|
|
1156
|
+
enabled: () => true,
|
|
1157
|
+
interval: 10,
|
|
1158
|
+
maxFailedAttempts: 3,
|
|
1159
|
+
reconnectIfFailed: () => true,
|
|
1160
|
+
maxFailedAttemptsToRetryReconnect: 10
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
let config = {
|
|
1164
|
+
uri: 'localhost:22222',
|
|
1165
|
+
buckets,
|
|
1166
|
+
prefix: 'tests:',
|
|
1167
|
+
ping,
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let redisProxy;
|
|
1171
|
+
let toxiproxy;
|
|
1172
|
+
let db;
|
|
1173
|
+
|
|
1174
|
+
beforeEach((done) => {
|
|
1175
|
+
toxiproxy = new Toxiproxy("http://localhost:8474");
|
|
1176
|
+
proxyBody = {
|
|
1177
|
+
listen: "0.0.0.0:22222",
|
|
1178
|
+
name: crypto.randomUUID(), //randomize name to avoid concurrency issues
|
|
1179
|
+
upstream: "redis:6379"
|
|
1180
|
+
};
|
|
1181
|
+
toxiproxy.createProxy(proxyBody)
|
|
1182
|
+
.then((proxy) => {
|
|
1183
|
+
redisProxy = proxy;
|
|
1184
|
+
done();
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
afterEach((done) => {
|
|
1190
|
+
redisProxy.remove().then(() =>
|
|
1191
|
+
db.close((err) => {
|
|
1192
|
+
// Can't close DB if it was never open
|
|
1193
|
+
if (err?.message.indexOf('enableOfflineQueue') > 0 || err?.message.indexOf('Connection is closed') >= 0) {
|
|
1194
|
+
err = undefined;
|
|
1195
|
+
}
|
|
1196
|
+
done(err);
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
it('should emit ping success', (done) => {
|
|
1202
|
+
db = createDB({ uri: 'localhost:22222', buckets, prefix: 'tests:', ping }, done)
|
|
1203
|
+
db.once(('ping'), (result) => {
|
|
1204
|
+
if (result.status === LimitDB.PING_SUCCESS) {
|
|
1205
|
+
done();
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
it('should emit "ping - error" when redis stops responding pings', (done) => {
|
|
1211
|
+
let called = false;
|
|
1212
|
+
|
|
1213
|
+
db = createDB(config, done)
|
|
1214
|
+
db.once(('ready'), () => addLatencyToxic(redisProxy, 20000, noop));
|
|
1215
|
+
db.on(('ping'), (result) => {
|
|
1216
|
+
if (result.status === LimitDB.PING_ERROR && !called) {
|
|
1217
|
+
called = true;
|
|
1218
|
+
db.removeAllListeners('ping');
|
|
1219
|
+
done();
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('should emit "ping - reconnect" when redis stops responding pings and client is configured to reconnect', (done) => {
|
|
1225
|
+
let called = false;
|
|
1226
|
+
db = createDB(config, done)
|
|
1227
|
+
db.once(('ready'), () => addLatencyToxic(redisProxy, 20000, noop));
|
|
1228
|
+
db.on(('ping'), (result) => {
|
|
1229
|
+
if (result.status === LimitDB.PING_RECONNECT && !called) {
|
|
1230
|
+
called = true;
|
|
1231
|
+
db.removeAllListeners('ping')
|
|
1232
|
+
done();
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('should emit "ping - reconnect dry run" when redis stops responding pings and client is NOT configured to reconnect', (done) => {
|
|
1238
|
+
let called = false;
|
|
1239
|
+
db = createDB({ ...config, ping: {...ping, reconnectIfFailed: () => false} }, done)
|
|
1240
|
+
db.once(('ready'), () => addLatencyToxic(redisProxy, 20000, noop));
|
|
1241
|
+
db.on(('ping'), (result) => {
|
|
1242
|
+
if (result.status === LimitDB.PING_RECONNECT_DRY_RUN && !called) {
|
|
1243
|
+
called = true;
|
|
1244
|
+
db.removeAllListeners('ping')
|
|
1245
|
+
done();
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it(`should NOT emit ping events when config.ping is not set`, (done) => {
|
|
1251
|
+
db = createDB({ ...config, ping: undefined }, done)
|
|
1252
|
+
|
|
1253
|
+
db.once(('ping'), (result) => {
|
|
1254
|
+
done(new Error(`unexpected ping event emitted ${result}`))
|
|
1255
|
+
})
|
|
1256
|
+
|
|
1257
|
+
//If after 100ms there are no interactions, we mark the test as passed.
|
|
1258
|
+
setTimeout(done, 100)
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('should recover from a connection loss', (done) => {
|
|
1262
|
+
let pingResponded = false;
|
|
1263
|
+
let reconnected = false;
|
|
1264
|
+
let toxic = undefined;
|
|
1265
|
+
let timeoutId;
|
|
1266
|
+
db = createDB({ ...config, ping: {...ping, interval: 50} }, done)
|
|
1267
|
+
|
|
1268
|
+
db.on(('ping'), (result) => {
|
|
1269
|
+
if (result.status === LimitDB.PING_SUCCESS) {
|
|
1270
|
+
if (!pingResponded) {
|
|
1271
|
+
pingResponded = true;
|
|
1272
|
+
toxic = addLatencyToxic(redisProxy, 20000, (t) => toxic = t);
|
|
1273
|
+
} else if (reconnected) {
|
|
1274
|
+
clearTimeout(timeoutId);
|
|
1275
|
+
db.removeAllListeners('ping')
|
|
1276
|
+
done();
|
|
1277
|
+
}
|
|
1278
|
+
} else if (result.status === LimitDB.PING_RECONNECT) {
|
|
1279
|
+
if (pingResponded && !reconnected ) {
|
|
1280
|
+
reconnected = true;
|
|
1281
|
+
toxic.remove();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
})
|
|
1285
|
+
|
|
1286
|
+
timeoutId = setTimeout(() => done(new Error("Not reconnected")), 1800);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
const createDB = (config, done) => {
|
|
1290
|
+
let tmpDB = new LimitDB(config)
|
|
1291
|
+
|
|
1292
|
+
tmpDB.on(('error'), (err) => {
|
|
1293
|
+
//As we actively close the connection, there might be network-related errors while attempting to reconnect
|
|
1294
|
+
if (err?.message.indexOf('enableOfflineQueue') > 0 || err?.message.indexOf('Command timed out') >= 0) {
|
|
1295
|
+
err = undefined;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (err) {
|
|
1299
|
+
console.log(err, err.message)
|
|
1300
|
+
done(err)
|
|
1301
|
+
}
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
return tmpDB
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const addLatencyToxic = (proxy, latency, callback) => {
|
|
1308
|
+
let toxic = new Toxic(
|
|
1309
|
+
proxy,
|
|
1310
|
+
{type: "latency", attributes: { latency: latency }}
|
|
1311
|
+
)
|
|
1312
|
+
proxy.addToxic(toxic).then(callback)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
const noop = () => {}
|
|
1318
|
+
});
|