@creator.co/wapi 1.8.4 → 1.8.5

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.
@@ -0,0 +1,772 @@
1
+ import { jest } from '@jest/globals'
2
+ import { APIGatewayProxyEvent, Context } from 'aws-lambda'
3
+ import { expect as c_expect } from 'chai'
4
+ import request from 'supertest'
5
+
6
+ import Globals from '../../../../src/Globals.js'
7
+ import Proxy from '../../../../src/Server/lib/container/Proxy.js'
8
+ import { GlobalRateLimitConfig } from '../../../../src/Server/Router.js'
9
+ import { defaultUrl } from '../../../Test.utils.js'
10
+
11
+ describe('Rate Limiting', () => {
12
+ // @ts-ignore
13
+ let mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
14
+ let proxy: Proxy | null = null
15
+
16
+ beforeAll(() => {
17
+ // @ts-ignore
18
+ mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
19
+ })
20
+
21
+ afterAll(() => {
22
+ mockExit.mockRestore()
23
+ })
24
+
25
+ beforeEach(() => {
26
+ mockExit.mockReset()
27
+ })
28
+
29
+ afterEach(async () => {
30
+ if (proxy) {
31
+ await proxy.unload()
32
+ proxy = null
33
+ }
34
+ // Give ports time to release
35
+ await new Promise(resolve => setTimeout(resolve, 100))
36
+ })
37
+
38
+ test('Rate limit is applied with default settings', async () => {
39
+ const port = 57001
40
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
41
+
42
+ const rateLimitConfig: GlobalRateLimitConfig = {
43
+ enabled: true,
44
+ windowMs: 1000, // 1 second window for testing
45
+ limit: 3, // Allow only 3 requests
46
+ }
47
+
48
+ proxy = new Proxy(
49
+ {
50
+ routes: [],
51
+ port,
52
+ rateLimit: rateLimitConfig,
53
+ },
54
+ async (event: APIGatewayProxyEvent, context: Context) => {
55
+ context.succeed({
56
+ body: JSON.stringify({ success: true }),
57
+ statusCode: 200,
58
+ })
59
+ }
60
+ )
61
+
62
+ await proxy.load()
63
+
64
+ // First 3 requests should succeed
65
+ for (let i = 0; i < 3; i++) {
66
+ const res = await request(url).get('/test').expect(200)
67
+ c_expect(res.body).to.deep.equal({ success: true })
68
+ }
69
+
70
+ // 4th request should be rate limited
71
+ const rateLimitedRes = await request(url).get('/test').expect(429)
72
+ c_expect(rateLimitedRes.body).to.have.property('error', 'rate_limit_exceeded')
73
+ c_expect(rateLimitedRes.body).to.have.property('message')
74
+
75
+ // Wait for window to reset
76
+ await new Promise(resolve => setTimeout(resolve, 1100))
77
+
78
+ // Should work again after window reset
79
+ const afterResetRes = await request(url).get('/test').expect(200)
80
+ c_expect(afterResetRes.body).to.deep.equal({ success: true })
81
+ }, 10000)
82
+
83
+ test('Rate limit headers are present', async () => {
84
+ const port = 57002
85
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
86
+
87
+ const rateLimitConfig: GlobalRateLimitConfig = {
88
+ enabled: true,
89
+ windowMs: 60000,
90
+ limit: 10,
91
+ }
92
+
93
+ proxy = new Proxy(
94
+ {
95
+ routes: [],
96
+ port,
97
+ rateLimit: rateLimitConfig,
98
+ },
99
+ async (event: APIGatewayProxyEvent, context: Context) => {
100
+ context.succeed({
101
+ body: JSON.stringify({ success: true }),
102
+ statusCode: 200,
103
+ })
104
+ }
105
+ )
106
+
107
+ await proxy.load()
108
+
109
+ const res = await request(url).get('/test').expect(200)
110
+
111
+ // Check for rate limit headers
112
+ c_expect(res.headers).to.have.property('ratelimit-limit')
113
+ c_expect(res.headers).to.have.property('ratelimit-remaining')
114
+ c_expect(res.headers).to.have.property('ratelimit-reset')
115
+
116
+ // Verify header values
117
+ c_expect(res.headers['ratelimit-limit']).to.equal('10')
118
+ c_expect(parseInt(res.headers['ratelimit-remaining'])).to.be.at.most(9)
119
+ })
120
+
121
+ test('Rate limit can be disabled', async () => {
122
+ const port = 57003
123
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
124
+
125
+ const rateLimitConfig: GlobalRateLimitConfig = {
126
+ enabled: false,
127
+ windowMs: 100,
128
+ limit: 2,
129
+ }
130
+
131
+ proxy = new Proxy(
132
+ {
133
+ routes: [],
134
+ port,
135
+ rateLimit: rateLimitConfig,
136
+ },
137
+ async (event: APIGatewayProxyEvent, context: Context) => {
138
+ context.succeed({
139
+ body: JSON.stringify({ success: true }),
140
+ statusCode: 200,
141
+ })
142
+ }
143
+ )
144
+
145
+ await proxy.load()
146
+
147
+ // Should be able to make many requests without hitting limit
148
+ for (let i = 0; i < 5; i++) {
149
+ const res = await request(url).get('/test').expect(200)
150
+ c_expect(res.body).to.deep.equal({ success: true })
151
+ }
152
+ })
153
+
154
+ test('Rate limit with custom handler', async () => {
155
+ const port = 57004
156
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
157
+ let customHandlerCalled = false
158
+
159
+ const rateLimitConfig: GlobalRateLimitConfig = {
160
+ enabled: true,
161
+ windowMs: 1000,
162
+ limit: 2,
163
+ handler: (req, res) => {
164
+ customHandlerCalled = true
165
+ res.status(429).json({
166
+ custom: true,
167
+ message: 'Custom rate limit message',
168
+ })
169
+ },
170
+ }
171
+
172
+ proxy = new Proxy(
173
+ {
174
+ routes: [],
175
+ port,
176
+ rateLimit: rateLimitConfig,
177
+ },
178
+ async (event: APIGatewayProxyEvent, context: Context) => {
179
+ context.succeed({
180
+ body: JSON.stringify({ success: true }),
181
+ statusCode: 200,
182
+ })
183
+ }
184
+ )
185
+
186
+ await proxy.load()
187
+
188
+ // First 2 requests succeed
189
+ await request(url).get('/test').expect(200)
190
+ await request(url).get('/test').expect(200)
191
+
192
+ // 3rd request hits custom handler
193
+ const rateLimitedRes = await request(url).get('/test').expect(429)
194
+ c_expect(customHandlerCalled).to.be.true
195
+ c_expect(rateLimitedRes.body).to.have.property('custom', true)
196
+ c_expect(rateLimitedRes.body).to.have.property('message', 'Custom rate limit message')
197
+ }, 10000)
198
+
199
+ test('Rate limit with skip function', async () => {
200
+ const port = 57005
201
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
202
+
203
+ const rateLimitConfig: GlobalRateLimitConfig = {
204
+ enabled: true,
205
+ windowMs: 1000,
206
+ limit: 2,
207
+ skip: req => {
208
+ // Skip rate limiting for requests with special header
209
+ return req.headers['x-skip-rate-limit'] === 'true'
210
+ },
211
+ }
212
+
213
+ proxy = new Proxy(
214
+ {
215
+ routes: [],
216
+ port,
217
+ rateLimit: rateLimitConfig,
218
+ },
219
+ async (event: APIGatewayProxyEvent, context: Context) => {
220
+ context.succeed({
221
+ body: JSON.stringify({ success: true }),
222
+ statusCode: 200,
223
+ })
224
+ }
225
+ )
226
+
227
+ await proxy.load()
228
+
229
+ // Use up the rate limit
230
+ await request(url).get('/test').expect(200)
231
+ await request(url).get('/test').expect(200)
232
+
233
+ // Normal request should be rate limited
234
+ await request(url).get('/test').expect(429)
235
+
236
+ // Request with skip header should succeed
237
+ const skippedRes = await request(url).get('/test').set('x-skip-rate-limit', 'true').expect(200)
238
+ c_expect(skippedRes.body).to.deep.equal({ success: true })
239
+ }, 10000)
240
+
241
+ test('Rate limit with custom key generator', async () => {
242
+ const port = 57006
243
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
244
+
245
+ const rateLimitConfig: GlobalRateLimitConfig = {
246
+ enabled: true,
247
+ windowMs: 1000,
248
+ limit: 2,
249
+ keyGenerator: req => {
250
+ // Use custom header for rate limiting key instead of IP
251
+ return (req.headers['x-user-id'] as string) || 'anonymous'
252
+ },
253
+ }
254
+
255
+ proxy = new Proxy(
256
+ {
257
+ routes: [],
258
+ port,
259
+ rateLimit: rateLimitConfig,
260
+ },
261
+ async (event: APIGatewayProxyEvent, context: Context) => {
262
+ context.succeed({
263
+ body: JSON.stringify({ success: true }),
264
+ statusCode: 200,
265
+ })
266
+ }
267
+ )
268
+
269
+ await proxy.load()
270
+
271
+ // User 1 makes 2 requests (uses up their limit)
272
+ await request(url).get('/test').set('x-user-id', 'user1').expect(200)
273
+ await request(url).get('/test').set('x-user-id', 'user1').expect(200)
274
+
275
+ // User 1's 3rd request should be rate limited
276
+ await request(url).get('/test').set('x-user-id', 'user1').expect(429)
277
+
278
+ // User 2 should still have their own limit
279
+ const user2Res = await request(url).get('/test').set('x-user-id', 'user2').expect(200)
280
+ c_expect(user2Res.body).to.deep.equal({ success: true })
281
+ }, 10000)
282
+
283
+ test('Rate limit without config does not apply limiting', async () => {
284
+ const port = 57007
285
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
286
+
287
+ proxy = new Proxy(
288
+ {
289
+ routes: [],
290
+ port,
291
+ // No rateLimit config
292
+ },
293
+ async (event: APIGatewayProxyEvent, context: Context) => {
294
+ context.succeed({
295
+ body: JSON.stringify({ success: true }),
296
+ statusCode: 200,
297
+ })
298
+ }
299
+ )
300
+
301
+ await proxy.load()
302
+
303
+ // Should be able to make many requests
304
+ for (let i = 0; i < 10; i++) {
305
+ const res = await request(url).get('/test').expect(200)
306
+ c_expect(res.body).to.deep.equal({ success: true })
307
+ }
308
+ })
309
+
310
+ test('Multiple requests from different IPs have separate limits', async () => {
311
+ const port = 57008
312
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
313
+
314
+ const rateLimitConfig: GlobalRateLimitConfig = {
315
+ enabled: true,
316
+ windowMs: 1000,
317
+ limit: 1, // Very strict: only 1 request per window
318
+ keyGenerator: req => {
319
+ // Use x-forwarded-for header to simulate different IPs
320
+ return (req.headers['x-forwarded-for'] as string) || req.ip || 'unknown'
321
+ },
322
+ }
323
+
324
+ proxy = new Proxy(
325
+ {
326
+ routes: [],
327
+ port,
328
+ rateLimit: rateLimitConfig,
329
+ },
330
+ async (event: APIGatewayProxyEvent, context: Context) => {
331
+ context.succeed({
332
+ body: JSON.stringify({ success: true }),
333
+ statusCode: 200,
334
+ })
335
+ }
336
+ )
337
+
338
+ await proxy.load()
339
+
340
+ // IP1 makes a request (uses their limit)
341
+ await request(url).get('/test').set('x-forwarded-for', '192.168.1.1').expect(200)
342
+
343
+ // IP1's second request should be rate limited
344
+ await request(url).get('/test').set('x-forwarded-for', '192.168.1.1').expect(429)
345
+
346
+ // IP2 should still have their own limit available
347
+ const ip2Res = await request(url).get('/test').set('x-forwarded-for', '192.168.1.2').expect(200)
348
+ c_expect(ip2Res.body).to.deep.equal({ success: true })
349
+
350
+ // IP2's second request should also be rate limited
351
+ await request(url).get('/test').set('x-forwarded-for', '192.168.1.2').expect(429)
352
+ }, 10000)
353
+
354
+ test('Rate limit with containerSetupHook', async () => {
355
+ const port = 57009
356
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
357
+ let hookCalled = false
358
+
359
+ const rateLimitConfig: GlobalRateLimitConfig = {
360
+ enabled: true,
361
+ windowMs: 1000,
362
+ limit: 5,
363
+ }
364
+
365
+ proxy = new Proxy(
366
+ {
367
+ routes: [],
368
+ port,
369
+ rateLimit: rateLimitConfig,
370
+ containerSetupHook: async (listener, app) => {
371
+ hookCalled = true
372
+ // Hook can be used to add custom middleware or configure the server
373
+ c_expect(listener).to.exist
374
+ c_expect(app).to.exist
375
+ },
376
+ },
377
+ async (event: APIGatewayProxyEvent, context: Context) => {
378
+ context.succeed({
379
+ body: JSON.stringify({ success: true }),
380
+ statusCode: 200,
381
+ })
382
+ }
383
+ )
384
+
385
+ await proxy.load()
386
+
387
+ // Verify hook was called
388
+ c_expect(hookCalled).to.be.true
389
+
390
+ // Verify rate limiting still works after hook
391
+ const res = await request(url).get('/test').expect(200)
392
+ c_expect(res.body).to.deep.equal({ success: true })
393
+
394
+ // Check rate limit headers are present
395
+ c_expect(res.headers).to.have.property('ratelimit-limit')
396
+ })
397
+
398
+ test('Rate limit with Redis store configuration', async () => {
399
+ const port = 57010
400
+
401
+ // Capture console logs to verify Redis store is configured
402
+ const originalLog = console.log
403
+ const logs: string[] = []
404
+ console.log = (...args: any[]) => {
405
+ logs.push(args.join(' '))
406
+ originalLog.apply(console, args)
407
+ }
408
+
409
+ // Mock Redis client with proper typing
410
+ const mockRedisClient: any = {
411
+ sendCommand: jest.fn(async () => 'OK'),
412
+ connect: jest.fn(async () => undefined),
413
+ disconnect: jest.fn(async () => undefined),
414
+ isOpen: true,
415
+ isReady: true,
416
+ }
417
+
418
+ const rateLimitConfig: GlobalRateLimitConfig = {
419
+ enabled: true,
420
+ windowMs: 1000,
421
+ limit: 3,
422
+ store: 'redis',
423
+ redis: {
424
+ client: mockRedisClient as any,
425
+ prefix: 'test:rl:',
426
+ },
427
+ }
428
+
429
+ proxy = new Proxy(
430
+ {
431
+ routes: [],
432
+ port,
433
+ rateLimit: rateLimitConfig,
434
+ },
435
+ async (event: APIGatewayProxyEvent, context: Context) => {
436
+ context.succeed({
437
+ body: JSON.stringify({ success: true }),
438
+ statusCode: 200,
439
+ })
440
+ }
441
+ )
442
+
443
+ await proxy.load()
444
+
445
+ // Restore console.log
446
+ console.log = originalLog
447
+
448
+ // Verify Redis store logging message
449
+ const redisStoreLog = logs.find(log => log.includes('[RATE-LIMIT] - Using Redis store'))
450
+ c_expect(redisStoreLog).to.exist
451
+
452
+ // Verify rate limiting is enabled
453
+ const rateLimitEnabledLog = logs.find(log =>
454
+ log.includes('[RATE-LIMIT] - Global rate limiting enabled')
455
+ )
456
+ c_expect(rateLimitEnabledLog).to.exist
457
+
458
+ // Note: We don't make actual requests in this test because the mock Redis client
459
+ // doesn't fully implement the RedisStore interface. This test verifies that:
460
+ // 1. Redis store configuration is recognized
461
+ // 2. Correct logging messages are emitted
462
+ // 3. The proxy loads successfully with Redis config
463
+ })
464
+
465
+ test('Rate limit logs indicate store type', async () => {
466
+ const port = 57011
467
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
468
+
469
+ // Capture console logs
470
+ const originalLog = console.log
471
+ const logs: string[] = []
472
+ console.log = (...args: any[]) => {
473
+ logs.push(args.join(' '))
474
+ originalLog.apply(console, args)
475
+ }
476
+
477
+ const rateLimitConfig: GlobalRateLimitConfig = {
478
+ enabled: true,
479
+ windowMs: 1000,
480
+ limit: 5,
481
+ store: 'memory', // Explicitly set to memory
482
+ }
483
+
484
+ proxy = new Proxy(
485
+ {
486
+ routes: [],
487
+ port,
488
+ rateLimit: rateLimitConfig,
489
+ },
490
+ async (event: APIGatewayProxyEvent, context: Context) => {
491
+ context.succeed({
492
+ body: JSON.stringify({ success: true }),
493
+ statusCode: 200,
494
+ })
495
+ }
496
+ )
497
+
498
+ await proxy.load()
499
+
500
+ // Restore console.log
501
+ console.log = originalLog
502
+
503
+ // Verify logging messages
504
+ const rateLimitEnabledLog = logs.find(log =>
505
+ log.includes('[RATE-LIMIT] - Global rate limiting enabled')
506
+ )
507
+ const memoryStoreLog = logs.find(log => log.includes('[RATE-LIMIT] - Using in-memory store'))
508
+
509
+ c_expect(rateLimitEnabledLog).to.exist
510
+ c_expect(memoryStoreLog).to.exist
511
+
512
+ // Make a request to verify it works
513
+ const res = await request(url).get('/test').expect(200)
514
+ c_expect(res.body).to.deep.equal({ success: true })
515
+ })
516
+
517
+ test('Rate limit uses default keyGenerator with IP fallback chain', async () => {
518
+ const port = 57012
519
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
520
+
521
+ // Don't provide a custom keyGenerator - should use default
522
+ const rateLimitConfig: GlobalRateLimitConfig = {
523
+ enabled: true,
524
+ windowMs: 1000,
525
+ limit: 2,
526
+ // NO keyGenerator provided - will use default IP-based logic
527
+ }
528
+
529
+ proxy = new Proxy(
530
+ {
531
+ routes: [],
532
+ port,
533
+ rateLimit: rateLimitConfig,
534
+ },
535
+ async (event: APIGatewayProxyEvent, context: Context) => {
536
+ context.succeed({
537
+ body: JSON.stringify({ success: true }),
538
+ statusCode: 200,
539
+ })
540
+ }
541
+ )
542
+
543
+ await proxy.load()
544
+
545
+ // Make requests without x-forwarded-for header
546
+ // Should use req.ip or req.socket.remoteAddress
547
+ const res1 = await request(url).get('/test').expect(200)
548
+ c_expect(res1.body).to.deep.equal({ success: true })
549
+
550
+ const res2 = await request(url).get('/test').expect(200)
551
+ c_expect(res2.body).to.deep.equal({ success: true })
552
+
553
+ // 3rd request should be rate limited (covers default handler lines 208-209)
554
+ const res3 = await request(url).get('/test').expect(429)
555
+ c_expect(res3.body).to.have.property('error', 'rate_limit_exceeded')
556
+ c_expect(res3.body).to.have.property('message', 'Too many requests. Please try again later.')
557
+ }, 10000)
558
+
559
+ test('Rate limit default keyGenerator uses x-forwarded-for', async () => {
560
+ const port = 57013
561
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
562
+
563
+ // Don't provide a custom keyGenerator
564
+ const rateLimitConfig: GlobalRateLimitConfig = {
565
+ enabled: true,
566
+ windowMs: 1000,
567
+ limit: 1, // Very strict
568
+ // NO keyGenerator - uses default with x-forwarded-for fallback
569
+ }
570
+
571
+ proxy = new Proxy(
572
+ {
573
+ routes: [],
574
+ port,
575
+ rateLimit: rateLimitConfig,
576
+ },
577
+ async (event: APIGatewayProxyEvent, context: Context) => {
578
+ context.succeed({
579
+ body: JSON.stringify({ success: true }),
580
+ statusCode: 200,
581
+ })
582
+ }
583
+ )
584
+
585
+ await proxy.load()
586
+
587
+ // Test with x-forwarded-for header (covers line 197-198)
588
+ const res1 = await request(url)
589
+ .get('/test')
590
+ .set('x-forwarded-for', '10.0.0.1, 10.0.0.2')
591
+ .expect(200)
592
+ c_expect(res1.body).to.deep.equal({ success: true })
593
+
594
+ // Second request from same IP should be rate limited
595
+ const res2 = await request(url)
596
+ .get('/test')
597
+ .set('x-forwarded-for', '10.0.0.1, 10.0.0.3')
598
+ .expect(429)
599
+ c_expect(res2.body).to.have.property('error', 'rate_limit_exceeded')
600
+
601
+ // Wait for window to reset
602
+ await new Promise(resolve => setTimeout(resolve, 1100))
603
+
604
+ // Different IP should work (covers the split and trim logic)
605
+ const res3 = await request(url).get('/test').set('x-forwarded-for', '10.0.0.99').expect(200)
606
+ c_expect(res3.body).to.deep.equal({ success: true })
607
+ }, 10000)
608
+
609
+ test('Rate limit default handler with warning log', async () => {
610
+ const port = 57014
611
+ const url = defaultUrl.replace(String(Globals.Listener_HTTP_DefaultPort), String(port))
612
+
613
+ // Capture console.warn to verify default handler logging
614
+ const originalWarn = console.warn
615
+ const warnings: any[] = []
616
+ console.warn = (...args: any[]) => {
617
+ warnings.push(args)
618
+ originalWarn.apply(console, args)
619
+ }
620
+
621
+ // Don't provide custom handler - should use default
622
+ const rateLimitConfig: GlobalRateLimitConfig = {
623
+ enabled: true,
624
+ windowMs: 1000,
625
+ limit: 1,
626
+ // NO custom handler - will use default (lines 208-209)
627
+ }
628
+
629
+ proxy = new Proxy(
630
+ {
631
+ routes: [],
632
+ port,
633
+ rateLimit: rateLimitConfig,
634
+ },
635
+ async (event: APIGatewayProxyEvent, context: Context) => {
636
+ context.succeed({
637
+ body: JSON.stringify({ success: true }),
638
+ statusCode: 200,
639
+ })
640
+ }
641
+ )
642
+
643
+ await proxy.load()
644
+
645
+ // Use up the limit
646
+ await request(url).get('/test-endpoint').expect(200)
647
+
648
+ // Trigger rate limit (should call default handler and log warning)
649
+ const rateLimitedRes = await request(url).get('/test-endpoint').expect(429)
650
+
651
+ // Restore console.warn
652
+ console.warn = originalWarn
653
+
654
+ // Verify default handler was called (lines 208-209, 220-225)
655
+ c_expect(rateLimitedRes.body).to.deep.equal({
656
+ error: 'rate_limit_exceeded',
657
+ message: 'Too many requests. Please try again later.',
658
+ })
659
+
660
+ // Verify warning was logged
661
+ const rateLimitWarning = warnings.find(w => w[0] === '[Proxy] - [RATE-LIMIT] - Limit exceeded')
662
+ c_expect(rateLimitWarning).to.exist
663
+ c_expect(rateLimitWarning[1]).to.have.property('path', '/test-endpoint')
664
+ c_expect(rateLimitWarning[1]).to.have.property('method', 'GET')
665
+ c_expect(rateLimitWarning[1]).to.have.property('timestamp')
666
+ }, 10000)
667
+
668
+ test('Rate limit with Redis store sendCommand and prefix', async () => {
669
+ const port = 57015
670
+
671
+ // Capture console logs
672
+ const originalLog = console.log
673
+ const logs: string[] = []
674
+ console.log = (...args: any[]) => {
675
+ logs.push(args.join(' '))
676
+ originalLog.apply(console, args)
677
+ }
678
+
679
+ // Mock Redis client
680
+ const sendCommandCalls: any[] = []
681
+ const mockRedisClient: any = {
682
+ sendCommand: jest.fn(async (...args: any[]) => {
683
+ sendCommandCalls.push(args)
684
+ return 'OK'
685
+ }),
686
+ }
687
+
688
+ // Test with custom prefix (covers lines 248-252)
689
+ const rateLimitConfig: GlobalRateLimitConfig = {
690
+ enabled: true,
691
+ windowMs: 1000,
692
+ limit: 3,
693
+ store: 'redis',
694
+ redis: {
695
+ client: mockRedisClient,
696
+ prefix: 'custom:prefix:', // Custom prefix (line 252)
697
+ },
698
+ }
699
+
700
+ proxy = new Proxy(
701
+ {
702
+ routes: [],
703
+ port,
704
+ rateLimit: rateLimitConfig,
705
+ },
706
+ async (event: APIGatewayProxyEvent, context: Context) => {
707
+ context.succeed({
708
+ body: JSON.stringify({ success: true }),
709
+ statusCode: 200,
710
+ })
711
+ }
712
+ )
713
+
714
+ await proxy.load()
715
+
716
+ // Restore console.log
717
+ console.log = originalLog
718
+
719
+ // Verify Redis store was created with correct logging (line 249)
720
+ const redisLog = logs.find(log => log.includes('[RATE-LIMIT] - Using Redis store'))
721
+ c_expect(redisLog).to.exist
722
+
723
+ // Verify RedisStore was instantiated (lines 250-252)
724
+ // The store object should have been created with sendCommand and prefix
725
+ // We can't directly test the RedisStore internals, but we verified:
726
+ // 1. Redis store logging message appears
727
+ // 2. The configuration is accepted without errors
728
+ // 3. Proxy loads successfully with Redis config
729
+ })
730
+
731
+ test('Rate limit with Redis store default prefix', async () => {
732
+ const port = 57016
733
+
734
+ const mockRedisClient: any = {
735
+ sendCommand: jest.fn(async () => 'OK'),
736
+ }
737
+
738
+ // Don't provide prefix - should use default 'wapi:rl:' (line 252)
739
+ const rateLimitConfig: GlobalRateLimitConfig = {
740
+ enabled: true,
741
+ windowMs: 1000,
742
+ limit: 5,
743
+ store: 'redis',
744
+ redis: {
745
+ client: mockRedisClient,
746
+ // NO prefix - should default to 'wapi:rl:' (line 252)
747
+ },
748
+ }
749
+
750
+ proxy = new Proxy(
751
+ {
752
+ routes: [],
753
+ port,
754
+ rateLimit: rateLimitConfig,
755
+ },
756
+ async (event: APIGatewayProxyEvent, context: Context) => {
757
+ context.succeed({
758
+ body: JSON.stringify({ success: true }),
759
+ statusCode: 200,
760
+ })
761
+ }
762
+ )
763
+
764
+ // Should load successfully with default prefix
765
+ await proxy.load()
766
+
767
+ // Verify proxy is running
768
+ c_expect(proxy).to.exist
769
+ })
770
+ })
771
+
772
+ export {}