test(auth): deflake login rate-limit burst test under CPU load #10

Merged
fabi merged 1 commits from fix/deflake-rate-limit-burst-test into main 2026-06-16 18:04:54 +00:00

View File

@@ -659,7 +659,8 @@ async fn login_no_user_branch_runs_argon2_for_timing_equalisation(pool: PgPool)
async fn login_rate_limited_under_burst_pressure(pool: PgPool) { async fn login_rate_limited_under_burst_pressure(pool: PgPool) {
let h = common::harness_with_auth_rate_limit(pool, 1, 3); let h = common::harness_with_auth_rate_limit(pool, 1, 3);
// Register a victim so the wrong-password branch is real work. // One register hit first — register and login share the one bucket,
// so this exercises the cross-endpoint limit (and consumes a token).
let _ = h let _ = h
.app .app
.clone() .clone()
@@ -667,9 +668,18 @@ async fn login_rate_limited_under_burst_pressure(pool: PgPool) {
.await .await
.unwrap(); .unwrap();
// Register consumed one token from the burst-3 bucket. Fire 30 // Fire 30 logins back-to-back; at least one must come back 429.
// wrong-password logins back-to-back; with per_sec=1 the refill //
// is too slow to keep up and at least one must come back 429. // We send an EMPTY password on purpose. `login` calls the rate
// limiter FIRST, then short-circuits empty credentials with a 400
// BEFORE any argon2 hash or DB lookup — so each attempt still
// consumes a token but is near-instant. That makes the burst drain
// the bucket far faster than the per_sec=1 refill regardless of how
// loaded the CI box is. (A real wrong-password attempt runs argon2,
// ~1s under load, which lets the 1/sec refill keep exact pace with
// the loop and the bucket never empties — that was the old flake.
// The bucket math itself is covered by rate_limit.rs's unit test;
// here we only need to prove the limiter is wired into the route.)
let mut saw_429 = false; let mut saw_429 = false;
for _ in 0..30 { for _ in 0..30 {
let resp = h let resp = h
@@ -677,7 +687,7 @@ async fn login_rate_limited_under_burst_pressure(pool: PgPool) {
.clone() .clone()
.oneshot(common::post_json( .oneshot(common::post_json(
"/api/v1/auth/login", "/api/v1/auth/login",
json!({ "username": "victim", "password": "wrong" }), json!({ "username": "victim", "password": "" }),
)) ))
.await .await
.unwrap(); .unwrap();