1use crate::address_ban_info::AddressBanInfo;
4use crate::Uri;
5use chrono::Utc;
6use rand::{rngs::SmallRng, seq::IteratorRandom, SeedableRng};
7use std::collections::hash_map::Entry;
8use std::collections::HashMap;
9use std::hash::{Hash, Hasher};
10use std::mem;
11use std::str::FromStr;
12use std::sync::{Arc, RwLock};
13use std::time::Duration;
14
15const DEFAULT_BASE_BAN_PERIOD: Duration = Duration::from_secs(60);
16
17#[derive(Debug, Clone, Eq)]
19#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
20pub struct Address(#[cfg_attr(feature = "mocks", serde(with = "http_serde::uri"))] Uri);
21
22impl FromStr for Address {
23 type Err = AddressListError;
24
25 fn from_str(s: &str) -> Result<Self, Self::Err> {
26 Uri::from_str(s)
27 .map_err(|e| AddressListError::InvalidAddressUri(e.to_string()))
28 .map(Address::try_from)?
29 }
30}
31
32impl PartialEq<Self> for Address {
33 fn eq(&self, other: &Self) -> bool {
34 self.0 == other.0
35 }
36}
37
38impl PartialEq<Uri> for Address {
39 fn eq(&self, other: &Uri) -> bool {
40 self.0 == *other
41 }
42}
43
44impl Hash for Address {
45 fn hash<H: Hasher>(&self, state: &mut H) {
46 self.0.hash(state);
47 }
48}
49
50impl TryFrom<Uri> for Address {
51 type Error = AddressListError;
52
53 fn try_from(value: Uri) -> Result<Self, Self::Error> {
54 if value.host().is_none() {
55 return Err(AddressListError::InvalidAddressUri(
56 "uri must contain host".to_string(),
57 ));
58 }
59
60 Ok(Address(value))
61 }
62}
63
64impl Address {
65 pub fn uri(&self) -> &Uri {
67 &self.0
68 }
69}
70
71#[derive(Debug, Default, Clone)]
74pub struct AddressStatus {
75 ban_count: usize,
76 banned_until: Option<chrono::DateTime<Utc>>,
77 ban_reason: Option<String>,
81}
82
83impl AddressStatus {
84 pub fn ban(&mut self, base_ban_period: &Duration) {
88 self.ban_with_reason(base_ban_period, None);
89 }
90
91 pub fn ban_with_reason(&mut self, base_ban_period: &Duration, reason: Option<String>) {
106 let coefficient = (self.ban_count as f64).exp();
107 let ban_period = Duration::from_secs_f64(base_ban_period.as_secs_f64() * coefficient);
108
109 self.banned_until = Some(chrono::Utc::now() + ban_period);
110 self.ban_count += 1;
111 self.ban_reason = reason;
112 }
113
114 pub fn ban_for(&mut self, period: Duration, reason: Option<String>) {
134 let advertised_until = chrono::Utc::now() + period;
135 if self
136 .banned_until
137 .map(|current| current < advertised_until)
138 .unwrap_or(true)
139 {
140 self.banned_until = Some(advertised_until);
141 self.ban_reason = reason;
142 }
143 self.ban_count = self.ban_count.max(1);
144 }
145
146 pub fn is_banned(&self) -> bool {
148 self.ban_count > 0
149 }
150
151 pub fn unban(&mut self) {
153 self.ban_count = 0;
154 self.banned_until = None;
155 self.ban_reason = None;
156 }
157}
158
159#[derive(Debug, thiserror::Error, Clone)]
161#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
162pub enum AddressListError {
163 #[error("unable parse address: {0}")]
165 #[cfg_attr(feature = "mocks", serde(skip))]
166 InvalidAddressUri(String),
167}
168
169#[derive(Debug, Clone)]
172pub struct AddressList {
173 addresses: Arc<RwLock<HashMap<Address, AddressStatus>>>,
174 base_ban_period: Duration,
175}
176
177impl Default for AddressList {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183impl std::fmt::Display for Address {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 self.0.fmt(f)
186 }
187}
188
189impl AddressList {
190 pub fn new() -> Self {
192 AddressList::with_settings(DEFAULT_BASE_BAN_PERIOD)
193 }
194
195 pub fn with_settings(base_ban_period: Duration) -> Self {
197 AddressList {
198 addresses: Arc::new(RwLock::new(HashMap::new())),
199 base_ban_period,
200 }
201 }
202
203 pub fn ban(&self, address: &Address) -> bool {
208 self.ban_with_reason(address, None)
209 }
210
211 pub fn ban_with_reason(&self, address: &Address, reason: Option<String>) -> bool {
214 let mut guard = self.addresses.write().unwrap();
215
216 let Some(status) = guard.get_mut(address) else {
217 return false;
218 };
219
220 status.ban_with_reason(&self.base_ban_period, reason);
221
222 true
223 }
224
225 pub fn ban_for(&self, address: &Address, period: Duration, reason: Option<String>) -> bool {
231 let mut guard = self.addresses.write().unwrap();
232
233 let Some(status) = guard.get_mut(address) else {
234 return false;
235 };
236
237 status.ban_for(period, reason);
238
239 true
240 }
241
242 pub fn unban(&self, address: &Address) -> bool {
245 let mut guard = self.addresses.write().unwrap();
246
247 let Some(status) = guard.get_mut(address) else {
248 return false;
249 };
250
251 status.unban();
252
253 true
254 }
255
256 pub fn is_banned(&self, address: &Address) -> bool {
258 let guard = self.addresses.read().unwrap();
259
260 guard
261 .get(address)
262 .map(|status| status.is_banned())
263 .unwrap_or(false)
264 }
265
266 pub fn add(&mut self, address: Address) -> bool {
269 let mut guard = self.addresses.write().unwrap();
270
271 match guard.entry(address) {
272 Entry::Occupied(_) => false,
273 Entry::Vacant(e) => {
274 e.insert(AddressStatus::default());
275
276 true
277 }
278 }
279 }
280
281 pub fn remove(&mut self, address: &Address) -> Option<AddressStatus> {
284 let mut guard = self.addresses.write().unwrap();
285
286 guard.remove(address)
287 }
288
289 #[deprecated]
290 pub fn add_uri(&mut self, uri: Uri) -> bool {
294 self.add(Address::try_from(uri).expect("valid uri"))
295 }
296
297 pub fn get_live_address(&self) -> Option<Address> {
302 let guard = self.addresses.read().unwrap();
305
306 let mut rng = SmallRng::from_entropy();
307 let now = chrono::Utc::now();
308
309 guard
310 .iter()
311 .filter(|(_, status)| {
312 status
313 .banned_until
314 .map(|banned_until| banned_until < now)
315 .unwrap_or(true)
316 })
317 .choose(&mut rng)
318 .map(|(addr, _)| addr.clone())
319 }
320
321 pub fn get_live_addresses(&self) -> Vec<Address> {
341 let guard = self.addresses.read().unwrap();
342
343 let now = chrono::Utc::now();
344
345 guard
346 .iter()
347 .filter(|(_, status)| {
348 status
349 .banned_until
350 .map(|banned_until| banned_until < now)
351 .unwrap_or(true)
352 })
353 .map(|(addr, _)| addr.clone())
354 .collect()
355 }
356
357 pub fn ban_info(&self) -> Vec<AddressBanInfo> {
366 let guard = self.addresses.read().unwrap();
367
368 let now = chrono::Utc::now();
369
370 guard
371 .iter()
372 .map(|(addr, status)| {
373 let banned = status.ban_count > 0
374 && status
375 .banned_until
376 .map(|banned_until| banned_until >= now)
377 .unwrap_or(false);
378 AddressBanInfo {
379 uri: addr.to_string(),
380 banned,
381 ban_count: status.ban_count,
382 banned_until: status.banned_until,
383 reason: status.ban_reason.clone(),
384 }
385 })
386 .collect()
387 }
388
389 pub fn len(&self) -> usize {
391 self.addresses.read().unwrap().len()
392 }
393
394 pub fn is_empty(&self) -> bool {
399 self.addresses.read().unwrap().is_empty()
400 }
401}
402
403impl IntoIterator for AddressList {
404 type Item = (Address, AddressStatus);
405 type IntoIter = std::collections::hash_map::IntoIter<Address, AddressStatus>;
406
407 fn into_iter(self) -> Self::IntoIter {
408 let mut guard = self.addresses.write().unwrap();
409
410 let addresses_map = mem::take(&mut *guard);
411
412 addresses_map.into_iter()
413 }
414}
415
416impl FromStr for AddressList {
417 type Err = AddressListError;
418
419 fn from_str(s: &str) -> Result<Self, Self::Err> {
420 let uri_list: Vec<Address> = s
421 .split(',')
422 .map(Address::from_str)
423 .collect::<Result<_, _>>()?;
424
425 Ok(Self::from_iter(uri_list))
426 }
427}
428
429impl FromIterator<Address> for AddressList {
430 fn from_iter<T: IntoIterator<Item = Address>>(iter: T) -> Self {
431 let mut address_list = Self::new();
432 for uri in iter {
433 address_list.add(uri);
434 }
435
436 address_list
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_get_live_addresses_empty_list() {
446 let list = AddressList::new();
447 let live_addresses = list.get_live_addresses();
448 assert_eq!(live_addresses.len(), 0);
449 }
450
451 #[test]
452 fn test_get_live_addresses_all_unbanned() {
453 let mut list = AddressList::new();
454 list.add("http://127.0.0.1:3000".parse().unwrap());
455 list.add("http://127.0.0.1:3001".parse().unwrap());
456 list.add("http://127.0.0.1:3002".parse().unwrap());
457
458 let live_addresses = list.get_live_addresses();
459 assert_eq!(live_addresses.len(), 3);
460 }
461
462 #[test]
463 fn test_get_live_addresses_some_banned() {
464 let mut list = AddressList::new();
465 let addr1: Address = "http://127.0.0.1:3000".parse().unwrap();
466 let addr2: Address = "http://127.0.0.1:3001".parse().unwrap();
467 let addr3: Address = "http://127.0.0.1:3002".parse().unwrap();
468
469 list.add(addr1.clone());
470 list.add(addr2.clone());
471 list.add(addr3.clone());
472
473 list.ban(&addr2);
475
476 let live_addresses = list.get_live_addresses();
477 assert_eq!(live_addresses.len(), 2);
478 assert!(live_addresses.contains(&addr1));
479 assert!(live_addresses.contains(&addr3));
480 assert!(!live_addresses.contains(&addr2));
481 }
482
483 #[test]
484 fn test_get_live_addresses_all_banned() {
485 let mut list = AddressList::new();
486 let addr1: Address = "http://127.0.0.1:3000".parse().unwrap();
487 let addr2: Address = "http://127.0.0.1:3001".parse().unwrap();
488
489 list.add(addr1.clone());
490 list.add(addr2.clone());
491
492 list.ban(&addr1);
494 list.ban(&addr2);
495
496 let live_addresses = list.get_live_addresses();
497 assert_eq!(live_addresses.len(), 0);
498 }
499
500 #[test]
501 fn test_get_live_addresses_unbanned_after_ban() {
502 let mut list = AddressList::new();
503 let addr1: Address = "http://127.0.0.1:3000".parse().unwrap();
504
505 list.add(addr1.clone());
506
507 list.ban(&addr1);
509 list.unban(&addr1);
510
511 let live_addresses = list.get_live_addresses();
512 assert_eq!(live_addresses.len(), 1);
513 assert!(live_addresses.contains(&addr1));
514 }
515
516 #[test]
517 fn test_address_try_from_uri_without_host() {
518 let uri: Uri = Uri::from_str("/path/only").unwrap();
519 let result = Address::try_from(uri);
520 assert!(result.is_err());
521 let err = result.unwrap_err();
522 assert!(matches!(err, AddressListError::InvalidAddressUri(_)));
523 }
524
525 #[test]
526 fn test_address_from_str_invalid_uri() {
527 let result = Address::from_str("not a valid uri\x00");
529 assert!(result.is_err());
530 }
531
532 #[test]
533 fn test_address_uri_accessor() {
534 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
535 let uri = addr.uri();
536 assert_eq!(uri.host(), Some("127.0.0.1"));
537 }
538
539 #[test]
540 fn test_address_partial_eq_with_uri() {
541 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
542 let uri = Uri::from_str("http://127.0.0.1:3000").unwrap();
543 assert!(addr == uri);
544
545 let other_uri = Uri::from_str("http://127.0.0.1:4000").unwrap();
546 assert!(addr != other_uri);
547 }
548
549 #[test]
550 fn test_address_display() {
551 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
552 let display = format!("{}", addr);
553 assert!(display.contains("127.0.0.1"));
554 }
555
556 #[test]
557 fn test_address_status_is_banned() {
558 let mut status = AddressStatus::default();
559 assert!(!status.is_banned());
560
561 status.ban(&Duration::from_secs(60));
562 assert!(status.is_banned());
563
564 status.unban();
565 assert!(!status.is_banned());
566 }
567
568 #[test]
569 fn test_address_status_exponential_ban() {
570 let mut status = AddressStatus::default();
571 let base_period = Duration::from_secs(1);
572
573 status.ban(&base_period);
575 assert_eq!(status.ban_count, 1);
576 assert!(status.banned_until.is_some());
577
578 status.ban(&base_period);
580 assert_eq!(status.ban_count, 2);
581 }
582
583 #[test]
584 fn test_address_list_is_empty() {
585 let list = AddressList::new();
586 assert!(list.is_empty());
587
588 let mut list = AddressList::new();
589 list.add("http://127.0.0.1:3000".parse().unwrap());
590 assert!(!list.is_empty());
591 }
592
593 #[test]
594 fn test_address_list_len() {
595 let mut list = AddressList::new();
596 assert_eq!(list.len(), 0);
597
598 list.add("http://127.0.0.1:3000".parse().unwrap());
599 assert_eq!(list.len(), 1);
600
601 list.add("http://127.0.0.1:3001".parse().unwrap());
602 assert_eq!(list.len(), 2);
603 }
604
605 #[test]
606 fn test_address_list_add_duplicate() {
607 let mut list = AddressList::new();
608 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
609
610 assert!(list.add(addr.clone()));
611 assert!(!list.add(addr)); assert_eq!(list.len(), 1);
613 }
614
615 #[test]
616 fn test_address_list_remove() {
617 let mut list = AddressList::new();
618 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
619
620 list.add(addr.clone());
621 assert_eq!(list.len(), 1);
622
623 let removed = list.remove(&addr);
624 assert!(removed.is_some());
625 assert_eq!(list.len(), 0);
626
627 let removed = list.remove(&addr);
629 assert!(removed.is_none());
630 }
631
632 #[test]
633 fn test_address_list_ban_nonexistent() {
634 let list = AddressList::new();
635 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
636 assert!(!list.ban(&addr));
637 }
638
639 #[test]
640 fn test_address_list_unban_nonexistent() {
641 let list = AddressList::new();
642 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
643 assert!(!list.unban(&addr));
644 }
645
646 #[test]
647 fn test_address_list_is_banned() {
648 let mut list = AddressList::new();
649 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
650 let unknown: Address = "http://127.0.0.1:9999".parse().unwrap();
651
652 list.add(addr.clone());
653
654 assert!(!list.is_banned(&addr));
655 assert!(!list.is_banned(&unknown)); list.ban(&addr);
658 assert!(list.is_banned(&addr));
659 }
660
661 #[test]
662 fn test_address_list_from_str() {
663 let list: AddressList = "http://127.0.0.1:3000,http://127.0.0.1:3001"
664 .parse()
665 .unwrap();
666 assert_eq!(list.len(), 2);
667 }
668
669 #[test]
670 fn test_address_list_from_str_single() {
671 let list: AddressList = "http://127.0.0.1:3000".parse().unwrap();
672 assert_eq!(list.len(), 1);
673 }
674
675 #[test]
676 fn test_address_list_from_str_invalid() {
677 let result: Result<AddressList, _> = "not a valid uri\x00".parse();
678 assert!(result.is_err());
679 }
680
681 #[test]
682 fn test_address_list_get_live_address_returns_none_when_empty() {
683 let list = AddressList::new();
684 assert!(list.get_live_address().is_none());
685 }
686
687 #[test]
688 fn test_address_list_get_live_address_returns_some_when_available() {
689 let mut list = AddressList::new();
690 list.add("http://127.0.0.1:3000".parse().unwrap());
691 assert!(list.get_live_address().is_some());
692 }
693
694 #[test]
695 fn test_address_list_into_iter() {
696 let mut list = AddressList::new();
697 list.add("http://127.0.0.1:3000".parse().unwrap());
698 list.add("http://127.0.0.1:3001".parse().unwrap());
699
700 let items: Vec<_> = list.into_iter().collect();
701 assert_eq!(items.len(), 2);
702 }
703
704 #[test]
705 fn test_address_list_with_settings() {
706 let list = AddressList::with_settings(Duration::from_secs(120));
707 assert!(list.is_empty());
708 }
709
710 #[test]
711 fn test_address_list_default() {
712 let list = AddressList::default();
713 assert!(list.is_empty());
714 }
715
716 #[test]
717 fn test_address_status_ban_with_reason_stores_reason() {
718 let mut status = AddressStatus::default();
719 assert!(status.ban_reason.is_none());
720
721 status.ban_with_reason(
722 &Duration::from_secs(60),
723 Some("transport error".to_string()),
724 );
725 assert_eq!(status.ban_reason.as_deref(), Some("transport error"));
726 assert!(status.is_banned());
727 }
728
729 #[test]
730 fn test_address_status_ban_without_reason_is_none() {
731 let mut status = AddressStatus::default();
732 status.ban(&Duration::from_secs(60));
733 assert!(status.ban_reason.is_none());
734 assert!(status.is_banned());
735 }
736
737 #[test]
738 fn test_address_status_unban_clears_reason() {
739 let mut status = AddressStatus::default();
740 status.ban_with_reason(&Duration::from_secs(60), Some("boom".to_string()));
741 assert_eq!(status.ban_reason.as_deref(), Some("boom"));
742
743 status.unban();
744 assert!(status.ban_reason.is_none());
745 assert!(!status.is_banned());
746 }
747
748 #[test]
749 fn test_address_list_ban_with_reason_records_reason() {
750 let mut list = AddressList::new();
751 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
752 list.add(addr.clone());
753
754 assert!(list.ban_with_reason(&addr, Some("node down".to_string())));
755
756 let info = list.ban_info();
757 assert_eq!(info.len(), 1);
758 let entry = &info[0];
759 assert_eq!(entry.reason.as_deref(), Some("node down"));
760 assert!(entry.banned);
761 assert_eq!(entry.ban_count, 1);
762 assert!(entry.banned_until.is_some());
763 }
764
765 #[test]
766 fn test_address_list_ban_without_reason_records_none() {
767 let mut list = AddressList::new();
768 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
769 list.add(addr.clone());
770
771 assert!(list.ban(&addr));
772
773 let info = list.ban_info();
774 assert_eq!(info.len(), 1);
775 assert!(info[0].reason.is_none());
776 assert!(info[0].banned);
777 }
778
779 #[test]
780 fn test_address_list_unban_clears_reason_in_ban_info() {
781 let mut list = AddressList::new();
782 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
783 list.add(addr.clone());
784
785 list.ban_with_reason(&addr, Some("oops".to_string()));
786 assert!(list.unban(&addr));
787
788 let info = list.ban_info();
789 assert_eq!(info.len(), 1);
790 let entry = &info[0];
791 assert!(entry.reason.is_none());
792 assert!(!entry.banned);
793 assert_eq!(entry.ban_count, 0);
794 assert!(entry.banned_until.is_none());
795 }
796
797 #[test]
798 fn test_ban_info_reflects_unbanned_address() {
799 let mut list = AddressList::new();
800 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
801 list.add(addr.clone());
802
803 let info = list.ban_info();
805 assert_eq!(info.len(), 1);
806 let entry = &info[0];
807 assert!(!entry.banned);
808 assert_eq!(entry.ban_count, 0);
809 assert!(entry.banned_until.is_none());
810 assert!(entry.reason.is_none());
811 assert!(entry.uri.contains("127.0.0.1"));
812 }
813
814 #[test]
815 fn test_ban_info_empty_list() {
816 let list = AddressList::new();
817 assert!(list.ban_info().is_empty());
818 }
819
820 #[test]
821 fn test_address_status_ban_for_sets_exact_window_and_min_ban_count() {
822 let mut status = AddressStatus::default();
823 assert_eq!(status.ban_count, 0);
824 assert!(status.banned_until.is_none());
825
826 let before = chrono::Utc::now();
827 status.ban_for(Duration::from_secs(45), Some("rate limited".into()));
828 let after = chrono::Utc::now();
829
830 assert_eq!(status.ban_count, 1, "ban_for sets ban_count to max(0,1)=1");
832
833 let until = status.banned_until.expect("banned_until must be set");
835 let lower = (until - before).num_milliseconds() as f64 / 1000.0;
836 let upper = (until - after).num_milliseconds() as f64 / 1000.0;
837 assert!(
838 lower >= 44.9,
839 "banned_until lower bound too short: {lower}s"
840 );
841 assert!(upper <= 45.1, "banned_until upper bound too long: {upper}s");
842 assert_eq!(status.ban_reason.as_deref(), Some("rate limited"));
843 }
844
845 #[test]
850 fn test_ban_for_raises_fresh_node_to_ladder_floor() {
851 let mut status = AddressStatus::default();
852 assert_eq!(status.ban_count, 0, "starts clean");
853
854 status.ban_for(Duration::from_secs(10), Some("rl".into()));
856 assert_eq!(
857 status.ban_count, 1,
858 "ban_for must raise ban_count 0 → 1 (ladder floor)"
859 );
860
861 let base = Duration::from_secs(60);
864 let before = chrono::Utc::now();
865 status.ban_with_reason(&base, None); let after = chrono::Utc::now();
867 assert_eq!(status.ban_count, 2);
868
869 let until = status.banned_until.expect("banned_until set");
870 let lo = (until - before).num_milliseconds() as f64 / 1000.0;
871 let hi = (until - after).num_milliseconds() as f64 / 1000.0;
872 let expected = 60.0_f64 * std::f64::consts::E; assert!(
874 lo >= expected - 0.5,
875 "window lower {lo:.1}s < expected {expected:.1}s (should escalate from floor 1)"
876 );
877 assert!(
878 hi <= expected + 0.5,
879 "window upper {hi:.1}s > expected {expected:.1}s"
880 );
881 }
882
883 #[test]
884 fn test_address_status_ban_for_does_not_inflate_existing_ban_count() {
885 let mut status = AddressStatus::default();
888 let base = Duration::from_secs(60);
889 status.ban_with_reason(&base, None); status.ban_with_reason(&base, None); status.ban_with_reason(&base, None); status.ban_for(Duration::from_secs(30), Some("rl".into()));
893 assert_eq!(
894 status.ban_count, 3,
895 "ban_for must not inflate ban_count above its existing value"
896 );
897 }
898
899 #[test]
900 fn test_address_list_ban_for_returns_false_for_unknown() {
901 let list = AddressList::new();
902 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
903 assert!(!list.ban_for(&addr, Duration::from_secs(5), None));
904 }
905
906 #[test]
907 fn test_address_list_ban_for_bans_known_address() {
908 let mut list = AddressList::new();
909 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
910 list.add(addr.clone());
911
912 assert!(list.ban_for(&addr, Duration::from_secs(60), Some("rl".into())));
913 assert!(list.get_live_address().is_none());
915 let info = list.ban_info();
917 assert_eq!(info.len(), 1);
918 assert!(info[0].banned);
919 assert_eq!(info[0].ban_count, 1);
920 }
921
922 #[test]
932 fn test_ban_for_address_re_enters_rotation_after_window_expires() {
933 let mut list = AddressList::new();
934 let addr: Address = "http://127.0.0.1:3000".parse().unwrap();
935 list.add(addr.clone());
936
937 assert!(list.ban_for(&addr, Duration::from_secs(300), Some("rl".into())));
939 assert!(
940 list.get_live_address().is_none(),
941 "node must be hidden during active ban window"
942 );
943
944 {
946 let mut guard = list.addresses.write().unwrap();
947 let status = guard.get_mut(&addr).expect("addr must be in list");
948 status.banned_until = Some(chrono::Utc::now() - Duration::from_secs(1));
949 }
950
951 assert!(
953 list.get_live_address().is_some(),
954 "address must re-enter rotation after ban window expires"
955 );
956 assert!(
959 list.is_banned(&addr),
960 "is_banned() must still be true after window expiry (ban_count not reset)"
961 );
962 }
963
964 #[test]
968 fn test_ban_ladder_windows_match_exponential_formula() {
969 let mut status = AddressStatus::default();
970 let base_secs = 60.0_f64;
971 let base = Duration::from_secs(60);
972
973 for n in 0..3usize {
974 let before = chrono::Utc::now();
976 status.ban(&base);
977 let after = chrono::Utc::now();
978
979 assert_eq!(status.ban_count, n + 1, "ban_count must increment");
980 let period = base_secs * (n as f64).exp();
981 let banned_until = status.banned_until.expect("banned_until is set");
982 let lower = (banned_until - before).num_milliseconds() as f64 / 1000.0;
983 let upper = (banned_until - after).num_milliseconds() as f64 / 1000.0;
984 assert!(
985 lower >= period - 0.05,
986 "ban #{} window lower bound {lower}s < expected {period}s",
987 n + 1
988 );
989 assert!(
990 upper <= period + 0.05,
991 "ban #{} window upper bound {upper}s > expected {period}s",
992 n + 1
993 );
994 }
995 }
996}