@@ -141,8 +141,9 @@ describe('Express Middleware', () => {
141
141
// Skip if NATS server not available
142
142
let natsLimiter ;
143
143
try {
144
+ const uniqueKey = 'nats-test-' + Date . now ( ) ;
144
145
app . get ( '/nats-test' , rateLimit ( {
145
- key : 'nats-test' ,
146
+ key : uniqueKey ,
146
147
maxTokens : 2 ,
147
148
window : '10s' ,
148
149
nats : {
@@ -280,4 +281,228 @@ describe('Express Middleware', () => {
280
281
assert . strictEqual ( configResolverCalls , 1 ) ;
281
282
} ) ;
282
283
} ) ;
284
+
285
+ describe ( 'Distributed Rate Limiting' , ( ) => {
286
+ it ( 'should share rate limits across multiple Express instances with NATS' , async function ( ) {
287
+ // Skip if NATS server not available
288
+ let app1 , app2 ;
289
+ let server1 , server2 ;
290
+
291
+ try {
292
+ // Create two Express apps with same NATS configuration
293
+ app1 = express ( ) ;
294
+ app2 = express ( ) ;
295
+
296
+ const natsConfig = {
297
+ servers : 'nats://localhost:4222' ,
298
+ bucket : 'test-express-distributed' ,
299
+ prefix : 'exp_dist_'
300
+ } ;
301
+
302
+ // Use same key and configuration for both apps
303
+ const uniqueKey = 'distributed-test-' + Date . now ( ) ;
304
+ const rateLimitConfig = {
305
+ key : uniqueKey ,
306
+ maxTokens : 10 ,
307
+ window : '10s' ,
308
+ nats : natsConfig
309
+ } ;
310
+
311
+ app1 . get ( '/distributed' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
312
+ res . json ( { message : 'success from app1' } ) ;
313
+ } ) ;
314
+
315
+ app2 . get ( '/distributed' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
316
+ res . json ( { message : 'success from app2' } ) ;
317
+ } ) ;
318
+
319
+ server1 = app1 . listen ( 0 ) ;
320
+ server2 = app2 . listen ( 0 ) ;
321
+
322
+ } catch ( err ) {
323
+ if ( err . message . includes ( 'NATS connection failed' ) ) {
324
+ console . log ( ' ⚠️ NATS server not available, skipping distributed middleware test' ) ;
325
+ this . skip ( ) ;
326
+ return ;
327
+ }
328
+ throw err ;
329
+ }
330
+
331
+ const port1 = server1 . address ( ) . port ;
332
+ const port2 = server2 . address ( ) . port ;
333
+
334
+ // Make 6 requests to app1
335
+ let app1Allowed = 0 ;
336
+ for ( let i = 0 ; i < 6 ; i ++ ) {
337
+ const res = await request ( app1 ) . get ( '/distributed' ) ;
338
+ if ( res . status === 200 ) app1Allowed ++ ;
339
+ }
340
+ assert . strictEqual ( app1Allowed , 6 ) ;
341
+
342
+ // Make 6 requests to app2 - should only allow 4 more
343
+ let app2Allowed = 0 ;
344
+ for ( let i = 0 ; i < 6 ; i ++ ) {
345
+ const res = await request ( app2 ) . get ( '/distributed' ) ;
346
+ if ( res . status === 200 ) app2Allowed ++ ;
347
+ }
348
+ assert . strictEqual ( app2Allowed , 4 ) ;
349
+
350
+ // Total should not exceed the limit
351
+ assert . strictEqual ( app1Allowed + app2Allowed , 10 ) ;
352
+
353
+ // Clean up
354
+ server1 . close ( ) ;
355
+ server2 . close ( ) ;
356
+ } ) ;
357
+
358
+ it ( 'should handle concurrent distributed requests correctly' , async function ( ) {
359
+ // Skip if NATS server not available
360
+ let app1 , app2 ;
361
+ let server1 , server2 ;
362
+
363
+ try {
364
+ // Create two Express apps
365
+ app1 = express ( ) ;
366
+ app2 = express ( ) ;
367
+
368
+ const natsConfig = {
369
+ servers : 'nats://localhost:4222' ,
370
+ bucket : 'test-express-concurrent' ,
371
+ prefix : 'exp_conc_'
372
+ } ;
373
+
374
+ const uniqueKey = 'concurrent-test-' + Date . now ( ) ;
375
+ const rateLimitConfig = {
376
+ key : uniqueKey ,
377
+ maxTokens : 50 ,
378
+ window : '10s' ,
379
+ nats : natsConfig
380
+ } ;
381
+
382
+ app1 . get ( '/concurrent' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
383
+ res . json ( { message : 'success' } ) ;
384
+ } ) ;
385
+
386
+ app2 . get ( '/concurrent' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
387
+ res . json ( { message : 'success' } ) ;
388
+ } ) ;
389
+
390
+ server1 = app1 . listen ( 0 ) ;
391
+ server2 = app2 . listen ( 0 ) ;
392
+
393
+ } catch ( err ) {
394
+ if ( err . message . includes ( 'NATS connection failed' ) ) {
395
+ console . log ( ' ⚠️ NATS server not available, skipping concurrent distributed test' ) ;
396
+ this . skip ( ) ;
397
+ return ;
398
+ }
399
+ throw err ;
400
+ }
401
+
402
+ // Make concurrent requests from both servers
403
+ const promises = [ ] ;
404
+
405
+ // 40 requests to app1
406
+ for ( let i = 0 ; i < 40 ; i ++ ) {
407
+ promises . push (
408
+ request ( app1 ) . get ( '/concurrent' )
409
+ . then ( res => res . status === 200 )
410
+ ) ;
411
+ }
412
+
413
+ // 40 requests to app2
414
+ for ( let i = 0 ; i < 40 ; i ++ ) {
415
+ promises . push (
416
+ request ( app2 ) . get ( '/concurrent' )
417
+ . then ( res => res . status === 200 )
418
+ ) ;
419
+ }
420
+
421
+ const results = await Promise . all ( promises ) ;
422
+ const totalAllowed = results . filter ( r => r ) . length ;
423
+
424
+ // Should respect the limit with some tolerance for race conditions
425
+ assert ( totalAllowed >= 48 && totalAllowed <= 52 ,
426
+ `Expected ~50 allowed requests, got ${ totalAllowed } ` ) ;
427
+
428
+ // Clean up
429
+ server1 . close ( ) ;
430
+ server2 . close ( ) ;
431
+ } ) ;
432
+
433
+ it ( 'should use distributed storage with configResolver' , async function ( ) {
434
+ // Skip if NATS server not available
435
+ let app1 , app2 ;
436
+ let server1 , server2 ;
437
+
438
+ try {
439
+ app1 = express ( ) ;
440
+ app2 = express ( ) ;
441
+
442
+ const natsConfig = {
443
+ servers : 'nats://localhost:4222' ,
444
+ bucket : 'test-express-resolver' ,
445
+ prefix : 'exp_res_'
446
+ } ;
447
+
448
+ const rateLimitConfig = {
449
+ keyGenerator : ( req ) => req . headers [ 'x-api-key' ] || 'anonymous' ,
450
+ configResolver : ( apiKey ) => {
451
+ if ( apiKey && apiKey . startsWith ( 'test-key-' ) ) {
452
+ return {
453
+ maxTokens : 5 ,
454
+ window : '10s'
455
+ } ;
456
+ }
457
+ return null ;
458
+ } ,
459
+ nats : natsConfig
460
+ } ;
461
+
462
+ app1 . get ( '/resolver' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
463
+ res . json ( { message : 'success from app1' } ) ;
464
+ } ) ;
465
+
466
+ app2 . get ( '/resolver' , rateLimit ( rateLimitConfig ) , ( req , res ) => {
467
+ res . json ( { message : 'success from app2' } ) ;
468
+ } ) ;
469
+
470
+ server1 = app1 . listen ( 0 ) ;
471
+ server2 = app2 . listen ( 0 ) ;
472
+
473
+ } catch ( err ) {
474
+ if ( err . message . includes ( 'NATS connection failed' ) ) {
475
+ console . log ( ' ⚠️ NATS server not available, skipping resolver distributed test' ) ;
476
+ this . skip ( ) ;
477
+ return ;
478
+ }
479
+ throw err ;
480
+ }
481
+
482
+ // Use unique test key to avoid conflicts
483
+ const testApiKey = 'test-key-' + Date . now ( ) ;
484
+
485
+ // Make 3 requests to app1 with test-key
486
+ for ( let i = 0 ; i < 3 ; i ++ ) {
487
+ const res = await request ( app1 )
488
+ . get ( '/resolver' )
489
+ . set ( 'x-api-key' , testApiKey ) ;
490
+ assert . strictEqual ( res . status , 200 ) ;
491
+ }
492
+
493
+ // Make 3 more requests to app2 - should only allow 2
494
+ let app2Allowed = 0 ;
495
+ for ( let i = 0 ; i < 3 ; i ++ ) {
496
+ const res = await request ( app2 )
497
+ . get ( '/resolver' )
498
+ . set ( 'x-api-key' , testApiKey ) ;
499
+ if ( res . status === 200 ) app2Allowed ++ ;
500
+ }
501
+ assert . strictEqual ( app2Allowed , 2 ) ;
502
+
503
+ // Clean up
504
+ server1 . close ( ) ;
505
+ server2 . close ( ) ;
506
+ } ) ;
507
+ } ) ;
283
508
} ) ;
0 commit comments