@@ -11,75 +11,101 @@ import { TestHarness } from "./test_utils";
1111import type { Context , MultiRegionContext , RegionContext } from "./types" ;
1212
1313type TestCase = {
14- // requests per second
15- rps : number ;
1614 /**
17- * Multiplier for rate
18- *
19- * rate = 10, load = 0.5 -> attack rate will be 5
15+ * Limit allowed during window
16+ */
17+ limit : number ;
18+ /**
19+ * Request load
20+ *
21+ * E.g., 0.5 means 50% of the limit in each window will be consumed,
22+ * so all requests will succeed (assuming rate=1)
23+ *
24+ * E.g., 2 means 200% of the limit in each window will be consumed,
25+ * so half of the requests will be rejected (assuming rate=1)
2026 */
2127 load : number ;
2228 /**
23- * rate at which the tokens will be added or consumed, default should be 1
24- * @default 1
29+ * rate at which the tokens will be added or consumed
2530 */
26- rate ? : number ;
31+ rate : number ;
2732} ;
28- const attackDuration = 10 ;
29- const window = 5 ;
33+ const attackDuration = 8 ;
34+ const window = 4 ;
3035const windowString : Duration = `${ window } s` ;
3136
3237const testcases : TestCase [ ] = [ ] ;
3338
34- for ( const rps of [ 10 , 100 ] ) {
35- for ( const load of [ 0.5 , 0.7 ] ) {
36- for ( const rate of [ undefined , 10 ] ) {
37- testcases . push ( { load, rps , rate } ) ;
39+ for ( const limit of [ 16 ] ) {
40+ for ( const load of [ 0.8 , 1.6 ] ) {
41+ for ( const rate of [ 1 , 3 ] ) {
42+ testcases . push ( { load, limit , rate } ) ;
3843 }
3944 }
4045}
4146
42- function run < TContext extends Context > ( builder : ( tc : TestCase ) => Ratelimit < TContext > ) {
47+ function run < TContext extends Context > (
48+ builder : ( tc : TestCase ) => Ratelimit < TContext >
49+ ) {
4350 for ( const tc of testcases ) {
44- const name = `${ tc . rps . toString ( ) . padStart ( 4 , " " ) } /s - Load: ${ ( tc . load * 100 )
45- . toString ( )
46- . padStart ( 3 , " " ) } % -> Sending ${ ( tc . rps * tc . load )
47- . toString ( )
48- . padStart ( 4 , " " ) } req/s at the rate of ${ tc . rate ?? 1 } `;
49- const ratelimit = builder ( tc ) ;
51+
52+ const windowCount = attackDuration / window ;
53+ /**
54+ * Total number of requests sent during the attack
55+ */
56+ const attackRequestCount = windowCount * tc . limit * tc . load ;
57+ /**
58+ * Number of requests the simulated attacker shall attempt
59+ */
60+ const attackRequestPerSecond = attackRequestCount / attackDuration ;
61+ /**
62+ * Maximum number of requests that can be allowed per second
63+ */
64+ const maxSuccessRequestCount = windowCount * tc . limit / tc . rate ;
65+ /**
66+ * Number of successful requests expected during the attack
67+ */
68+ const expectedSuccessRequestCount = Number . parseFloat ( Math . min ( maxSuccessRequestCount , attackRequestCount ) . toFixed ( 2 ) ) ;
5069
5170 const limits = {
52- lte : ( ( attackDuration * tc . rps * ( tc . rate ?? 1 ) ) / window ) * 1.5 ,
53- gte : ( ( attackDuration * tc . rps ) / window ) * 0.5 ,
71+ lte : Number . parseFloat ( ( expectedSuccessRequestCount * 1.5 ) . toFixed ( 2 ) ) ,
72+ gte : Number . parseFloat ( ( expectedSuccessRequestCount * 0.5 ) . toFixed ( 2 ) ) ,
5473 } ;
74+
75+ const name = `${ tc . limit } Limit, ${ tc . load * 100 } % Load, ${ attackRequestPerSecond } req/s (with rate=${ tc . rate } )` ;
76+ const range = `Range: ${ limits . gte } - ${ limits . lte } Success`
77+
78+ const ratelimit = builder ( tc ) ;
79+
5580 describe ( name , ( ) => {
5681 test (
57- `should be within ${ limits . gte } - ${ limits . lte } ` ,
82+ range ,
5883 async ( ) => {
59- log ( name ) ;
84+ log ( ) ;
85+ log ( ` Config: ${ name } ` ) ;
86+ log ( ` ${ range } (Expected: ${ expectedSuccessRequestCount } )` ) ;
6087 const harness = new TestHarness ( ratelimit ) ;
61- await harness . attack ( tc . rps * tc . load , attackDuration , tc . rate ) . catch ( ( error ) => {
62- console . error ( error ) ;
63- } ) ;
88+ await harness
89+ . attack ( attackRequestPerSecond , attackDuration , tc . rate )
90+ . catch ( ( error ) => {
91+ console . error ( error ) ;
92+ } ) ;
6493 log (
65- "success:" ,
66- harness . metrics . success ,
67- ", blocked:" ,
68- harness . metrics . rejected ,
69- "out of:" ,
70- harness . metrics . requests ,
94+ ` Result: success: ${ harness . metrics . success } , blocked: ${ harness . metrics . rejected } (out of: ${ harness . metrics . requests } )`
7195 ) ;
7296
7397 expect ( harness . metrics . success ) . toBeLessThanOrEqual ( limits . lte ) ;
7498 expect ( harness . metrics . success ) . toBeGreaterThanOrEqual ( limits . gte ) ;
7599 } ,
76- attackDuration * 1000 * 4 ,
100+ attackDuration * 1000 * 4
77101 ) ;
78102 } ) ;
79103 }
80104}
81105
82- function newMultiRegion ( limiter : Algorithm < MultiRegionContext > ) : Ratelimit < MultiRegionContext > {
106+ function newMultiRegion (
107+ limiter : Algorithm < MultiRegionContext >
108+ ) : Ratelimit < MultiRegionContext > {
83109 // eslint-disable-next-line unicorn/consistent-function-scoping
84110 function ensureEnv ( key : string ) : string {
85111 const value = process . env [ key ] ;
@@ -109,7 +135,9 @@ function newMultiRegion(limiter: Algorithm<MultiRegionContext>): Ratelimit<Multi
109135 } ) ;
110136}
111137
112- function newRegion ( limiter : Algorithm < RegionContext > ) : Ratelimit < RegionContext > {
138+ function newRegion (
139+ limiter : Algorithm < RegionContext >
140+ ) : Ratelimit < RegionContext > {
113141 return new RegionRatelimit ( {
114142 prefix : crypto . randomUUID ( ) ,
115143 redis : Redis . fromEnv ( ) ,
@@ -146,32 +174,44 @@ describe("timeout", () => {
146174
147175describe ( "fixedWindow" , ( ) => {
148176 describe ( "region" , ( ) =>
149- run ( ( tc ) => newRegion ( RegionRatelimit . fixedWindow ( tc . rps * ( tc . rate ?? 1 ) , windowString ) ) ) ) ;
177+ run ( ( tc ) =>
178+ newRegion ( RegionRatelimit . fixedWindow ( tc . limit , windowString ) )
179+ ) ) ;
150180
151181 describe ( "multiRegion" , ( ) =>
152182 run ( ( tc ) =>
153- newMultiRegion ( MultiRegionRatelimit . fixedWindow ( tc . rps * ( tc . rate ?? 1 ) , windowString ) ) ,
183+ newMultiRegion (
184+ MultiRegionRatelimit . fixedWindow ( tc . limit , windowString )
185+ )
154186 ) ) ;
155187} ) ;
156188describe ( "slidingWindow" , ( ) => {
157189 describe ( "region" , ( ) =>
158- run ( ( tc ) => newRegion ( RegionRatelimit . slidingWindow ( tc . rps * ( tc . rate ?? 1 ) , windowString ) ) ) ) ;
190+ run ( ( tc ) =>
191+ newRegion ( RegionRatelimit . slidingWindow ( tc . limit , windowString ) )
192+ ) ) ;
159193 describe ( "multiRegion" , ( ) =>
160194 run ( ( tc ) =>
161- newMultiRegion ( MultiRegionRatelimit . slidingWindow ( tc . rps * ( tc . rate ?? 1 ) , windowString ) ) ,
195+ newMultiRegion (
196+ MultiRegionRatelimit . slidingWindow ( tc . limit , windowString )
197+ )
162198 ) ) ;
163199} ) ;
164200
165201describe ( "tokenBucket" , ( ) => {
166202 describe ( "region" , ( ) =>
167203 run ( ( tc ) =>
168- newRegion ( RegionRatelimit . tokenBucket ( tc . rps , windowString , tc . rps * ( tc . rate ?? 1 ) ) ) ,
204+ newRegion (
205+ RegionRatelimit . tokenBucket ( tc . limit , windowString , tc . limit )
206+ )
169207 ) ) ;
170208} ) ;
171209
172210describe ( "cachedFixedWindow" , ( ) => {
173211 describe ( "region" , ( ) =>
174212 run ( ( tc ) =>
175- newRegion ( RegionRatelimit . cachedFixedWindow ( tc . rps * ( tc . rate ?? 1 ) , windowString ) ) ,
213+ newRegion (
214+ RegionRatelimit . cachedFixedWindow ( tc . limit , windowString )
215+ )
176216 ) ) ;
177217} ) ;
0 commit comments