#[tokio::test] async fn opaque_cookie_is_removed_from_origin_requests_and_protected_on_responses() { let pool = pool(Algorithm::RoundRobin, StickyMode::Cookie); let first = select(&pool); let cookie = first.cookie.unwrap().to_str().unwrap().to_owned(); assert!( cookie.contains("HttpOnly") && cookie.contains("SameSite=Lax") && !cookie.contains("Secure") ); let cookie = cookie.split(';').next().unwrap(); let mut headers = HeaderMap::new(); headers.insert("cookie", format!("{cookie}; app=ok").parse().unwrap()); let next = pool .select("10.0.0.1".parse().unwrap(), &headers, false) .unwrap(); assert_eq!( next.lease.member.definition.id, first.lease.member.definition.id ); assert!(next.cookie.is_none()); pool.strip_affinity(&mut headers); assert_eq!(headers["cookie"], "app=ok"); headers.append( "set-cookie", format!("{}=origin-spoof", pool.cookie_name) .parse() .unwrap(), ); headers.append("set-cookie", "app=ok".parse().unwrap()); pool.strip_affinity_response(&mut headers); assert_eq!(headers.get_all("set-cookie").iter().count(), 1); assert_eq!(headers["set-cookie"], "app=ok"); let secure = pool .select("127.0.0.1".parse().unwrap(), &HeaderMap::new(), true) .unwrap(); assert!(secure .cookie .unwrap() .to_str() .unwrap() .contains("; Secure")); } #[tokio::test] async fn failed_sticky_member_is_replaced_and_duplicate_cookies_are_ignored() { let pool = pool(Algorithm::RoundRobin, StickyMode::Cookie); let first = select(&pool); let cookie = first .cookie .unwrap() .to_str() .unwrap() .split(';') .next() .unwrap() .to_owned(); let mut headers = HeaderMap::new(); headers.insert("cookie", format!("{cookie}; {cookie}").parse().unwrap()); assert!(pool.affinity(&headers).is_none()); headers.insert("cookie", cookie.parse().unwrap()); lock(&first.lease.member.state.health).healthy = false; let next = pool .select("127.0.0.1".parse().unwrap(), &headers, false) .unwrap(); assert_ne!( next.lease.member.definition.id, first.lease.member.definition.id ); assert!(next.cookie.is_some()); lock(&next.lease.member.state.health).healthy = false; assert!(pool .select("127.0.0.1".parse().unwrap(), &headers, false) .is_none()); } #[tokio::test] async fn slow_start_ramps_weight_and_error_is_counted_once_per_exchange() { let pool = pool(Algorithm::LeastConnections, StickyMode::Off); let selected = select(&pool); let state = &selected.lease.member.state; assert!(state.weight(100, 100) < 100.0); assert_eq!(state.weight(100, 0), 100.0); selected.lease.started(); selected.lease.bytes(50, true); selected.lease.bytes(100, false); selected.lease.error("timeout", Duration::from_secs(1)); selected.lease.error("body", Duration::from_secs(2)); selected.lease.retry_attempted(); selected.lease.retry_succeeded(); assert_eq!(state.requests.load(Ordering::Relaxed), 1); assert_eq!(state.errors.load(Ordering::Relaxed), 1); assert_eq!(state.timeouts_total.load(Ordering::Relaxed), 1); assert_eq!(state.io_errors_total.load(Ordering::Relaxed), 1); assert_eq!(state.bytes_in.load(Ordering::Relaxed), 50); assert_eq!(state.bytes_out.load(Ordering::Relaxed), 100); assert_eq!(state.retry_attempts_total.load(Ordering::Relaxed), 1); assert_eq!(state.retry_successes_total.load(Ordering::Relaxed), 1); assert!(pool .prometheus() .contains("rust_proxy_backend_requests_total{vhost=\"app\",backend=\"a\"} 1")); assert!(pool .prometheus() .contains("rust_proxy_backend_retry_attempts_total{vhost=\"app\",backend=\"a\"} 1")); } #[tokio::test] async fn slow_5xx_counts_both_observability_classes_when_passive_is_off() { let pool = pool(Algorithm::LeastConnections, StickyMode::Off); let selected = select(&pool); selected.lease.response(500, Duration::from_secs(20), true); let state = &selected.lease.member.state; assert_eq!(state.http_5xx_total.load(Ordering::Relaxed), 1); assert_eq!(state.slow_responses_total.load(Ordering::Relaxed), 1); assert_eq!( state.passive_health_failures_total.load(Ordering::Relaxed), 0 ); assert!(state.healthy()); } #[tokio::test] async fn client_upload_duration_does_not_trigger_passive_slow_failure() { let pool = pool(Algorithm::LeastConnections, StickyMode::Off); let upload = select(&pool); upload.lease.response(200, Duration::from_secs(20), false); assert_eq!(upload.lease.member.state.errors.load(Ordering::Relaxed), 0); let read = select(&pool); read.lease.response(200, Duration::from_secs(20), true); assert_eq!(read.lease.member.state.errors.load(Ordering::Relaxed), 1); } #[tokio::test] async fn backup_is_only_new_assignment_when_no_primary_is_eligible() { let mut pool = pool(Algorithm::RoundRobin, StickyMode::Off); Arc::get_mut(&mut Arc::get_mut(&mut pool).unwrap().members[1]) .unwrap() .definition .backup = true; for _ in 0..8 { assert_eq!(select(&pool).lease.member.definition.id, "a"); } lock(&pool.members[0].state.health).healthy = false; assert_eq!(select(&pool).lease.member.definition.id, "b"); lock(&pool.members[0].state.health).healthy = true; assert_eq!(select(&pool).lease.member.definition.id, "a"); } #[tokio::test] async fn drain_rejects_new_sessions_but_preserves_valid_existing_affinity() { let mut pool = pool(Algorithm::RoundRobin, StickyMode::Cookie); Arc::get_mut(&mut Arc::get_mut(&mut pool).unwrap().members[0]) .unwrap() .definition .drain = true; assert_eq!(select(&pool).lease.member.definition.id, "b"); let mut headers = HeaderMap::new(); headers.insert( "cookie", format!("{}={}", pool.cookie_name, pool.members[0].token) .parse() .unwrap(), ); let existing = pool .select("127.0.0.1".parse().unwrap(), &headers, false) .unwrap(); assert_eq!(existing.lease.member.definition.id, "a"); lock(&pool.members[0].state.health).healthy = false; assert_eq!( pool.select("127.0.0.1".parse().unwrap(), &headers, false) .unwrap() .lease .member .definition .id, "b" ); }