use std::{fs, path::PathBuf, process::Command};
use nlink::{
Result,
netlink::{link::DummyLink, namespace, namespace::NamespaceSpec},
};
use crate::common::TestNamespace;
#[tokio::test]
async fn test_spawn_ip_link_show() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-ip")?;
let mut cmd = Command::new("ip");
cmd.arg("link");
let output = namespace::spawn_output(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("lo"), "should contain loopback interface");
Ok(())
}
#[tokio::test]
async fn test_spawn_sees_namespace_interface() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-if")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("test0")).await?;
conn.set_link_up("test0").await?;
let mut cmd = Command::new("ip");
cmd.arg("link");
let output = namespace::spawn_output(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("test0"), "should see test0 in namespace");
Ok(())
}
#[tokio::test]
async fn test_spawn_background_process() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-bg")?;
let mut cmd = Command::new("sleep");
cmd.arg("60");
let mut child = namespace::spawn(ns.name(), cmd)?;
let try_wait = child.try_wait().map_err(nlink::Error::Io)?;
assert!(try_wait.is_none(), "process should still be running");
child.kill().map_err(nlink::Error::Io)?;
child.wait().map_err(nlink::Error::Io)?;
Ok(())
}
#[tokio::test]
async fn test_spawn_captures_stdout() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-out")?;
let mut cmd = Command::new("echo");
cmd.arg("hello from namespace");
let output = namespace::spawn_output(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "hello from namespace");
Ok(())
}
#[tokio::test]
async fn test_spawn_nonexistent_namespace() -> Result<()> {
require_root!();
let result = namespace::spawn("definitely_does_not_exist_12345", Command::new("true"));
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_spawn_nonexistent_binary() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-nobin")?;
let result = namespace::spawn(ns.name(), Command::new("nonexistent_binary_12345"));
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_spawn_parent_namespace_unaffected() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-parent")?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("nsonly0")).await?;
let mut cmd = Command::new("ip");
cmd.arg("link");
let output = namespace::spawn_output(ns.name(), cmd)?;
let ns_stdout = String::from_utf8_lossy(&output.stdout);
assert!(ns_stdout.contains("nsonly0"));
let parent_output = Command::new("ip")
.arg("link")
.output()
.map_err(nlink::Error::Io)?;
let parent_stdout = String::from_utf8_lossy(&parent_output.stdout);
assert!(
!parent_stdout.contains("nsonly0"),
"parent namespace should not have nsonly0"
);
Ok(())
}
#[tokio::test]
async fn test_namespace_spec_spawn() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-spec")?;
let spec = NamespaceSpec::Named(ns.name());
let mut cmd = Command::new("echo");
cmd.arg("via spec");
let output = spec.spawn_output(cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "via spec");
Ok(())
}
fn setup_etc_netns(ns_name: &str, files: &[(&str, &str)]) -> PathBuf {
let dir = PathBuf::from("/etc/netns").join(ns_name);
fs::create_dir_all(&dir).expect("create /etc/netns/<name>/");
for (filename, content) in files {
fs::write(dir.join(filename), content).expect("write etc file");
}
dir
}
fn cleanup_etc_netns(ns_name: &str) {
let dir = PathBuf::from("/etc/netns").join(ns_name);
let _ = fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_spawn_with_etc_hosts() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-etc-h")?;
let custom_hosts = "127.0.0.1 custom-host.lab\n";
setup_etc_netns(ns.name(), &[("hosts", custom_hosts)]);
let mut cmd = Command::new("cat");
cmd.arg("/etc/hosts");
let output = namespace::spawn_output_with_etc(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
cleanup_etc_netns(ns.name());
assert!(
stdout.contains("custom-host.lab"),
"spawned process should see custom /etc/hosts, got: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_spawn_with_etc_no_dir() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-etc-nd")?;
let mut cmd = Command::new("echo");
cmd.arg("ok");
let output = namespace::spawn_output_with_etc(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "ok");
Ok(())
}
#[tokio::test]
async fn test_spawn_with_etc_host_unaffected() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-etc-ha")?;
let custom_hosts = "127.0.0.1 only-in-namespace\n";
setup_etc_netns(ns.name(), &[("hosts", custom_hosts)]);
let mut cmd = Command::new("cat");
cmd.arg("/etc/hosts");
let _ = namespace::spawn_output_with_etc(ns.name(), cmd)?;
let host_hosts = fs::read_to_string("/etc/hosts").expect("read host /etc/hosts");
cleanup_etc_netns(ns.name());
assert!(
!host_hosts.contains("only-in-namespace"),
"host /etc/hosts should NOT contain namespace-specific entry"
);
Ok(())
}
#[tokio::test]
async fn test_spawn_with_etc_sys_remount() -> Result<()> {
require_root!();
let ns = TestNamespace::new("spawn-etc-sys")?;
setup_etc_netns(ns.name(), &[("hosts", "127.0.0.1 localhost\n")]);
let mut cmd = Command::new("ls");
cmd.arg("/sys/class/net/");
let output = namespace::spawn_output_with_etc(ns.name(), cmd)?;
let stdout = String::from_utf8_lossy(&output.stdout);
cleanup_etc_netns(ns.name());
assert!(
stdout.contains("lo"),
"should see loopback in /sys/class/net/"
);
Ok(())
}