//! Integration-level coverage of [`aura_cli::pool::IpPool`]. //! //! The unit tests inside `src/pool.rs` cover the strategy matrix in isolation. These tests //! exercise the same surface as an external crate would — through the public re-exports — so //! that the API contract stays usable for downstream consumers (the `aura server` runtime). use std::collections::HashMap; use std::net::IpAddr; use aura_cli::pool::{IpPool, PoolStrategy}; use ipnetwork::IpNetwork; fn ip(s: &str) -> IpAddr { s.parse().unwrap() } fn net(s: &str) -> IpNetwork { s.parse().unwrap() } /// Smoke: build a tiny /29 pool, allocate, release, re-allocate the same address. #[tokio::test] async fn pool_allocate_release_cycle() { let pool = IpPool::new( net("10.8.0.0/29"), PoolStrategy::DynamicOnly, HashMap::new(), ip("10.8.0.1"), ) .expect("pool"); let a = pool.assign("a").await.expect("first"); let b = pool.assign("b").await.expect("second"); assert_ne!(a, b, "distinct clients must get distinct addresses"); let snap_before = pool.in_use_snapshot().await; assert!(snap_before.contains(&a)); assert!(snap_before.contains(&b)); pool.release(a).await; let snap_after = pool.in_use_snapshot().await; assert!(!snap_after.contains(&a)); // Next dynamic allocation can hand back `a`'s address. let again = pool.assign("c").await.expect("recycled"); assert_eq!(again, a, "released ip should be reusable"); } /// `StaticOrDynamic` honours a statically reserved address and never hands it to anyone else. #[tokio::test] async fn pool_static_reservation_is_pinned() { let mut statics = HashMap::new(); statics.insert("phone-1".to_string(), ip("10.8.0.20")); let pool = IpPool::new( net("10.8.0.0/24"), PoolStrategy::StaticOrDynamic, statics, ip("10.8.0.1"), ) .expect("pool"); assert_eq!(pool.assign("phone-1").await, Some(ip("10.8.0.20"))); // Many dynamic allocations: none of them must collide with the static reservation. for cid in ["a", "b", "c", "d", "e"] { let got = pool.assign(cid).await.expect("dynamic"); assert_ne!(got, ip("10.8.0.20"), "{cid} got the static reservation"); } } /// `StaticOnly` refuses unknown ids — the strict deployment mode. #[tokio::test] async fn pool_static_only_refuses_unknown() { let mut statics = HashMap::new(); statics.insert("phone-1".to_string(), ip("10.8.0.20")); let pool = IpPool::new( net("10.8.0.0/24"), PoolStrategy::StaticOnly, statics, ip("10.8.0.1"), ) .expect("pool"); assert!(pool.assign("phone-1").await.is_some()); assert!( pool.assign("randomer").await.is_none(), "StaticOnly must refuse unknown ids" ); } /// Exhausting the pool returns `None` instead of looping forever or panicking. #[tokio::test] async fn pool_exhaustion_returns_none() { // /30 -> 4 total, .0 net, .1 server, .2 usable, .3 broadcast => 1 dynamic slot. let pool = IpPool::new( net("10.8.0.0/30"), PoolStrategy::DynamicOnly, HashMap::new(), ip("10.8.0.1"), ) .expect("pool"); assert_eq!(pool.assign("a").await, Some(ip("10.8.0.2"))); assert!(pool.assign("b").await.is_none(), "pool exhausted"); }