Skip to main content

max / makenotwork

Fix auth rate limiter hitting GET /login, add unique test IPs GET /login was incorrectly rate-limited because route_layer applied the governor to both GET and POST on /login. Move rate limiter to per-handler .layer() so only POST /login, passkey start, and passkey finish are rate-limited. Also assign unique IPs per TestClient via atomic counter to prevent cross-test rate limit interference. Fix UUID type in sandbox subscription test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-02 19:23 UTC
Commit: 4d6d5dbf2a9b81098bc5f93645474c2e8cfd8d39
Parent: 87dad57
3 files changed, +19 insertions, -10 deletions
@@ -2,6 +2,7 @@
2 2
3 3 use axum::{
4 4 extract::State,
5 + handler::Handler,
5 6 http::{header::HeaderMap, StatusCode},
6 7 response::{Html, IntoResponse, Redirect, Response},
7 8 routing::{get, post},
@@ -35,13 +36,14 @@ pub fn auth_routes() -> Router<AppState> {
35 36 let validate_rate_limit = rate_limiter_per_sec(constants::VALIDATE_RATE_LIMIT_PER_SEC, constants::VALIDATE_RATE_LIMIT_BURST);
36 37
37 38 Router::new()
38 - // Auth routes with rate limiting
39 - .route("/login", post(login_handler))
40 - .route("/auth/passkey/start", post(passkey_auth_start))
41 - .route("/auth/passkey/finish", post(passkey_auth_finish))
42 - .route_layer(GovernorLayer {
43 - config: auth_rate_limit,
44 - })
39 + // GET /login is NOT rate-limited (page render for CSRF tokens).
40 + // POST /login and passkey routes ARE rate-limited.
41 + .route("/login", get(crate::routes::pages::public::landing::login_page)
42 + .post(login_handler.layer(GovernorLayer { config: auth_rate_limit.clone() })))
43 + .route("/auth/passkey/start", post(passkey_auth_start)
44 + .layer(GovernorLayer { config: auth_rate_limit.clone() }))
45 + .route("/auth/passkey/finish", post(passkey_auth_finish)
46 + .layer(GovernorLayer { config: auth_rate_limit }))
45 47 // Routes without auth rate limiting
46 48 .route("/logout", post(logout_handler))
47 49 .route("/auth/me", get(me_handler))
@@ -8,8 +8,12 @@ use axum::http::{header, Method, Request, StatusCode};
8 8 use axum::Router;
9 9 use http_body_util::BodyExt;
10 10 use std::collections::HashMap;
11 + use std::sync::atomic::{AtomicU32, Ordering};
11 12 use tower::ServiceExt;
12 13
14 + /// Monotonic counter for unique per-test IPs (10.1.x.y).
15 + static IP_COUNTER: AtomicU32 = AtomicU32::new(1);
16 +
13 17 /// A test HTTP client that talks to the app in-process.
14 18 pub struct TestClient {
15 19 app: Router,
@@ -21,11 +25,14 @@ pub struct TestClient {
21 25
22 26 impl TestClient {
23 27 pub fn new(app: Router) -> Self {
28 + let n = IP_COUNTER.fetch_add(1, Ordering::Relaxed);
29 + let octet3 = (n / 256) % 256;
30 + let octet4 = n % 256;
24 31 TestClient {
25 32 app,
26 33 cookies: HashMap::new(),
27 34 csrf_token: None,
28 - forwarded_ip: "127.0.0.1".to_string(),
35 + forwarded_ip: format!("10.1.{}.{}", octet3, octet4),
29 36 bearer_token: None,
30 37 }
31 38 }
@@ -496,7 +496,7 @@ async fn subscriber_tier_visibility() {
496 496 h.client.post_form("/logout", "").await;
497 497
498 498 // ── Visit the public project page as anonymous user ──
499 - let resp = h.client.get("/@tiervis/tier-vis").await;
499 + let resp = h.client.get("/p/tier-vis").await;
500 500 assert_eq!(resp.status, 200, "Public project page should render, got {}", resp.status);
501 501 // The project page template should include the tier name
502 502 assert!(
@@ -522,7 +522,7 @@ async fn sandbox_tier_uses_fake_stripe_ids() {
522 522 h.client.fetch_csrf_token().await;
523 523
524 524 // Find the sandbox user
525 - let (sandbox_user_id, is_sandbox): (i32, bool) = sqlx::query_as(
525 + let (sandbox_user_id, is_sandbox): (uuid::Uuid, bool) = sqlx::query_as(
526 526 "SELECT id, is_sandbox FROM users WHERE username LIKE 'sandbox_%' ORDER BY created_at DESC LIMIT 1",
527 527 )
528 528 .fetch_one(&h.db)