2024-11-15 23:51:18 +08:00
import { assert , assertEquals } from "@std/assert" ;
import { LockoutTimer } from "./lockout.ts" ;
import { FakeTime } from "@std/testing/time" ;
const lockoutTime = 60000 ;
const lockoutLimit = 10 ;
2024-11-15 23:50:50 +08:00
Deno . test ( "Lockout - failed login rate limiter" , ( ) = > {
2024-11-15 23:51:18 +08:00
using time = new FakeTime ( ) ;
testDisabled ( new LockoutTimer ( NaN , lockoutLimit ) , "by period" ) ;
testDisabled ( new LockoutTimer ( lockoutTime , NaN ) , "by limit" ) ;
2024-11-15 23:50:50 +08:00
testLockout ( new LockoutTimer ( lockoutTime , 10 ) , "explicit params" ) ;
testLockoutPerMS ( new LockoutTimer ( lockoutTime , 10 ) , "explicit params" ) ;
2024-11-15 23:51:18 +08:00
function testDisabled ( timer : LockoutTimer , txt : string ) {
for ( let i = 0 ; i < 100 ; i ++ ) {
assertEquals (
timer . isLocked ( ) ,
false ,
` Should be unlocked - disabled ${ txt } loop ${ i } ` ,
) ;
timer . addCount ( ) ;
}
}
function testLockoutPerMS ( timer : LockoutTimer , txt : string ) {
assert (
lockoutTime > lockoutLimit ,
` testLockoutPerMS assumes lockoutTime( ${ lockoutTime } ) > lockoutLimit( ${ lockoutLimit } ), so it can fill a bucket in 1ms jumps ` ,
) ;
const bucketPasses = 2 ;
let countLocked = 0 ;
let countUnLocked = 0 ;
const totalTests = lockoutTime * bucketPasses ;
for ( let i = 0 ; i < totalTests ; i ++ ) {
if ( timer . isLocked ( ) ) {
countLocked ++ ;
} else {
countUnLocked ++ ;
}
timer . addCount ( ) ;
time . tick ( 1 ) ;
}
// usually this will be bucketPasses+1 buckets
// but if time aligns could be just bucketPasses buckets
// and could be in between if it doesn't have time to completely fill the extra bucket
const expectedUnlocked = lockoutLimit * ( bucketPasses + 1 ) ;
const expectedLocked = totalTests - expectedUnlocked ;
const expectedMinUnlocked = lockoutLimit * bucketPasses ;
const expectedMaxLocked = totalTests - expectedMinUnlocked ;
assert (
countUnLocked >= expectedMinUnlocked && countUnLocked <= expectedUnlocked ,
` Expected between ${ expectedMinUnlocked } and ${ expectedUnlocked } unlocks in ${ bucketPasses } passes of ${ lockoutTime } ms, but got ${ countUnLocked } ( ${ txt } ) ` ,
) ;
assert (
countLocked >= expectedLocked && countLocked <= expectedMaxLocked ,
` Expected between ${ expectedLocked } and ${ expectedMaxLocked } locks in ${ bucketPasses } passes of ${ lockoutTime } ms, but got ${ countLocked } ( ${ txt } ) ` ,
) ;
}
function testLockout ( timer : LockoutTimer , txt : string ) {
for ( let pass = 0 ; pass < 3 ; pass ++ ) {
for ( let i = 0 ; i < lockoutLimit ; i ++ ) {
assertEquals (
timer . isLocked ( ) ,
false ,
` Should be unlocked pass ${ pass } loop ${ i } ${ txt } ` ,
) ;
timer . addCount ( ) ;
}
// count = lockoutLimit
for ( let i = 0 ; i < 10 ; i ++ ) {
assertEquals (
timer . isLocked ( ) ,
true ,
` Should be locked before bucket rollover. pass ${ pass } loop ${ i } ${ txt } ` ,
) ;
timer . addCount ( ) ;
}
// count = lockoutLimit + 10
time . tick ( lockoutTime ) ;
// bucket should rollover, count=0
}
}
} ) ;