use nlink::{
Result,
netlink::{
config::{ApplyOptions, DeclaredLinkType, LinkState, NetworkConfig},
link::DummyLink,
},
};
use crate::common::TestNamespace;
#[test]
fn test_network_config_builder() {
let config = NetworkConfig::new()
.link("dummy0", |l| l.dummy().up())
.link("br0", |l| l.bridge().up().mtu(9000));
assert_eq!(config.links().len(), 2);
assert_eq!(config.links()[0].name(), "dummy0");
assert_eq!(config.links()[0].link_type(), &DeclaredLinkType::Dummy);
assert_eq!(config.links()[0].state(), LinkState::Up);
assert_eq!(config.links()[1].name(), "br0");
assert_eq!(config.links()[1].link_type(), &DeclaredLinkType::Bridge);
assert_eq!(config.links()[1].mtu(), Some(9000));
}
#[test]
fn test_address_parsing() {
let config = NetworkConfig::new()
.address("eth0", "192.168.1.1/24")
.unwrap()
.address("eth0", "2001:db8::1/64")
.unwrap();
assert_eq!(config.addresses().len(), 2);
let addr1 = &config.addresses()[0];
assert_eq!(addr1.dev(), "eth0");
assert!(addr1.is_ipv4());
assert_eq!(addr1.prefix_len(), 24);
let addr2 = &config.addresses()[1];
assert!(addr2.is_ipv6());
assert_eq!(addr2.prefix_len(), 64);
}
#[test]
fn test_address_parsing_errors() {
let result = NetworkConfig::new().address("eth0", "192.168.1.1");
assert!(result.is_err());
let result = NetworkConfig::new().address("eth0", "invalid/24");
assert!(result.is_err());
let result = NetworkConfig::new().address("eth0", "192.168.1.1/abc");
assert!(result.is_err());
let result = NetworkConfig::new().address("eth0", "192.168.1.1/33");
assert!(result.is_err());
}
#[test]
fn test_route_parsing() {
let config = NetworkConfig::new()
.route("10.0.0.0/8", |r| r.via("192.168.1.1").dev("eth0"))
.unwrap()
.route("0.0.0.0/0", |r| r.via("192.168.1.254"))
.unwrap();
assert_eq!(config.routes().len(), 2);
let route1 = &config.routes()[0];
assert!(route1.is_ipv4());
assert_eq!(route1.prefix_len(), 8);
assert!(route1.gateway().is_some());
assert_eq!(route1.dev(), Some("eth0"));
let route2 = &config.routes()[1];
assert_eq!(route2.prefix_len(), 0); }
#[test]
fn test_route_parsing_errors() {
let result = NetworkConfig::new().route("10.0.0.0", |r| r);
assert!(result.is_err());
let result = NetworkConfig::new().route("invalid/8", |r| r);
assert!(result.is_err());
}
#[test]
fn test_qdisc_builder() {
let config = NetworkConfig::new()
.qdisc("eth0", |q| q.netem().delay_ms(100).loss(1.0))
.qdisc("eth1", |q| q.htb().default_class(0x30));
assert_eq!(config.qdiscs().len(), 2);
assert_eq!(config.qdiscs()[0].dev(), "eth0");
assert_eq!(config.qdiscs()[0].qdisc_type().kind(), "netem");
assert_eq!(config.qdiscs()[1].qdisc_type().kind(), "htb");
}
#[test]
fn test_veth_link_builder() {
let config = NetworkConfig::new().link("veth0", |l| l.veth("veth1").master("br0").up());
let link = &config.links()[0];
assert_eq!(link.name(), "veth0");
assert!(matches!(
link.link_type(),
DeclaredLinkType::Veth { peer } if peer == "veth1"
));
assert_eq!(link.master(), Some("br0"));
assert_eq!(link.state(), LinkState::Up);
}
#[test]
fn test_vlan_link_builder() {
let config = NetworkConfig::new().link("eth0.100", |l| l.vlan("eth0", 100).up());
let link = &config.links()[0];
assert!(matches!(
link.link_type(),
DeclaredLinkType::Vlan { parent, vlan_id } if parent == "eth0" && *vlan_id == 100
));
}
#[tokio::test]
async fn test_config_diff_empty_namespace() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-diff")?;
let conn = ns.connection()?;
let config = NetworkConfig::new();
let diff = config.diff(&conn).await?;
assert!(diff.is_empty());
assert_eq!(diff.change_count(), 0);
Ok(())
}
#[tokio::test]
async fn test_config_diff_detects_missing_link() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-diff-link")?;
let conn = ns.connection()?;
let config = NetworkConfig::new().link("dummy0", |l| l.dummy().up());
let diff = config.diff(&conn).await?;
assert!(!diff.is_empty());
assert_eq!(diff.links_to_add.len(), 1);
assert_eq!(diff.links_to_add[0].name(), "dummy0");
Ok(())
}
#[tokio::test]
async fn test_config_diff_detects_existing_link() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-diff-existing")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("dummy0")).await?;
let config = NetworkConfig::new().link("dummy0", |l| l.dummy());
let diff = config.diff(&conn).await?;
assert!(diff.links_to_add.is_empty());
Ok(())
}
#[tokio::test]
async fn test_config_diff_detects_state_change() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-diff-state")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("dummy0")).await?;
let config = NetworkConfig::new().link("dummy0", |l| l.dummy().up());
let diff = config.diff(&conn).await?;
assert_eq!(diff.links_to_modify.len(), 1);
assert_eq!(diff.links_to_modify[0].0, "dummy0");
assert!(diff.links_to_modify[0].1.set_up);
Ok(())
}
#[tokio::test]
async fn test_config_apply_creates_link() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-apply-link")?;
let conn = ns.connection()?;
let config = NetworkConfig::new().link("dummy0", |l| l.dummy().up());
let result = config.apply(&conn).await?;
assert!(result.is_success());
assert!(result.changes_made > 0);
let link = conn.get_link_by_name("dummy0").await?;
assert!(link.is_some());
Ok(())
}
#[tokio::test]
async fn test_config_apply_creates_address() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-apply-addr")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("dummy0")).await?;
conn.set_link_up("dummy0").await?;
let config = NetworkConfig::new()
.address("dummy0", "192.168.100.1/24")
.unwrap();
let result = config.apply(&conn).await?;
assert!(result.is_success());
let addrs = conn.get_addresses_by_name("dummy0").await?;
assert!(!addrs.is_empty());
Ok(())
}
#[tokio::test]
async fn test_config_apply_idempotent() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-idempotent")?;
let conn = ns.connection()?;
let config = NetworkConfig::new()
.link("dummy0", |l| l.dummy().up())
.address("dummy0", "10.0.0.1/24")
.unwrap();
let result1 = config.apply(&conn).await?;
assert!(result1.changes_made > 0);
let result2 = config.apply(&conn).await?;
assert_eq!(result2.changes_made, 0);
Ok(())
}
#[tokio::test]
async fn test_config_dry_run() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-dryrun")?;
let conn = ns.connection()?;
let config = NetworkConfig::new().link("dummy0", |l| l.dummy());
let result = config
.apply_with_options(
&conn,
ApplyOptions {
dry_run: true,
..Default::default()
},
)
.await?;
assert!(result.changes_made > 0);
let link = conn.get_link_by_name("dummy0").await?;
assert!(link.is_none());
Ok(())
}
#[tokio::test]
async fn test_config_apply_bridge_with_port() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-bridge")?;
let conn = ns.connection()?;
let config = NetworkConfig::new()
.link("br0", |l| l.bridge().up())
.link("dummy0", |l| l.dummy().master("br0").up());
let result = config.apply(&conn).await?;
assert!(result.is_success());
let br = conn.get_link_by_name("br0").await?;
assert!(br.is_some());
let dummy = conn.get_link_by_name("dummy0").await?;
assert!(dummy.is_some());
assert!(dummy.unwrap().master().is_some());
Ok(())
}
#[tokio::test]
async fn test_config_diff_summary() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-summary")?;
let conn = ns.connection()?;
let config = NetworkConfig::new()
.link("dummy0", |l| l.dummy().up())
.address("dummy0", "10.0.0.1/24")
.unwrap();
let diff = config.diff(&conn).await?;
let summary = diff.summary();
assert!(summary.contains("dummy0"));
assert!(summary.contains("10.0.0.1/24"));
Ok(())
}
#[tokio::test]
async fn test_config_apply_qdisc() -> Result<()> {
require_root!();
let ns = TestNamespace::new("config-qdisc")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("dummy0")).await?;
conn.set_link_up("dummy0").await?;
let config = NetworkConfig::new().qdisc("dummy0", |q| q.netem().delay_ms(50));
let result = config.apply(&conn).await?;
assert!(result.is_success());
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let netem = qdiscs.iter().find(|q| q.kind() == Some("netem"));
assert!(netem.is_some());
Ok(())
}