//! A faithful fake NetworkManager served as a real D-Bus service on a //! private `dbus-daemon`, so the production `nm` module (a pure zbus client //! against `org.freedesktop.NetworkManager` on the system bus) can be //! exercised end-to-end — real D-Bus marshalling, real property reads, real //! method calls — with zero test-only code paths in `src/`. //! //! Two ways to use it: //! //! - [`launch_private`] — starts a fresh private daemon + fake NM and //! returns a [`FakeNmBus`] handle. The caller passes //! `DBUS_SYSTEM_BUS_ADDRESS=` to any subprocess (the CLI sandbox) so //! the binary's `Connection::system()` lands on this bus. Independent per //! test → safe to run in parallel. //! - [`shared`] — one process-wide fake NM bus pointed at by the process //! env var (in-process tests can't set per-test env safely). Tests using //! it must serialize against each other, which the returned guard does. //! //! The fake implements the subset of the NetworkManager D-Bus API the //! client uses: devices, access points (SSID/strength/security flags), //! connection profiles (list/add/get/delete), activation (which lands the //! device on the matching AP and marks it ACTIVATED), per-device IP4Config, //! and the `WirelessEnabled`/`Connectivity` root properties. It also //! records every call for assertions. use std::collections::{BTreeMap, HashMap}; use std::io::BufRead; use std::ops::Deref; use std::process::{Child, Command, Stdio}; use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use zbus::fdo; use zbus::interface; use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; pub const NM_DEST: &str = "org.freedesktop.NetworkManager"; pub const NM_PATH: &str = "/org/freedesktop/NetworkManager"; pub const SETTINGS_PATH: &str = "/org/freedesktop/NetworkManager/Settings"; const DEVICES_PREFIX: &str = "/org/freedesktop/NetworkManager/Devices"; const APS_PREFIX: &str = "/org/freedesktop/NetworkManager/AccessPoints"; const ACTIVE_PREFIX: &str = "/org/freedesktop/NetworkManager/ActiveConnection"; const IP4_PREFIX: &str = "/org/freedesktop/NetworkManager/IP4Config"; const CONNS_PREFIX: &str = "/org/freedesktop/NetworkManager/Settings"; // Security flag constants (mirrors src/nm.rs). pub const DEV_TYPE_WIFI: u32 = 2; pub const DEV_STATE_ACTIVATED: u32 = 100; const AP_FLAG_PRIVACY: u32 = 0x1; const SEC_PSK: u32 = 0x100; const SEC_802_1X: u32 = 0x200; const SEC_SAE: u32 = 0x400; /// Wi-Fi security flavors the fake can advertise for an AP. #[derive(Debug, Clone, Copy)] pub enum Security { Open, Wpa2, Wpa3, Wpa1Wpa2, Enterprise, Wep, } impl Security { fn to_flags(self) -> (u32, u32, u32) { match self { Security::Open => (0, 0, 0), Security::Wpa2 => (AP_FLAG_PRIVACY, 0, SEC_PSK), Security::Wpa3 => (AP_FLAG_PRIVACY, 0, SEC_SAE), Security::Wpa1Wpa2 => (AP_FLAG_PRIVACY, SEC_PSK, SEC_PSK), Security::Enterprise => (AP_FLAG_PRIVACY, SEC_802_1X, SEC_802_1X), Security::Wep => (AP_FLAG_PRIVACY, 0, 0), } } } pub type SettingsMap = HashMap>; #[derive(Debug, Clone)] pub struct FakeDeviceData { pub iface: String, pub dev_type: u32, pub state: u32, pub active_ap: Option, pub ip4: String, } #[derive(Debug, Clone)] pub struct FakeApData { pub ssid: Vec, pub strength: u8, pub flags: u32, pub wpa: u32, pub rsn: u32, } #[derive(Debug, Default)] pub struct FakeState { pub devices: BTreeMap, pub aps: BTreeMap, pub dev_aps: HashMap>, pub connections: BTreeMap, /// active-conn path -> settings-conn path pub active_conns: BTreeMap, /// device path -> active-conn path pub dev_active: HashMap, pub connectivity: u32, pub wireless_enabled: bool, /// When set, activation lands the device on the AP with this SSID /// (simulates an NM autoconnect race landing elsewhere). pub land_on: Option, /// When set, activation creates a matching AP on the fly if none exists /// (hidden networks appear in the scan only after connecting). pub connect_any: bool, /// When > 0, the next activations fail (simulating a transient NM /// failure); each attempted activation decrements the counter. pub fail_next_activations: u32, pub calls: Vec, pub next_dev: u32, pub next_ap: u32, pub next_conn: u32, pub next_active: u32, } #[derive(Clone)] struct Shared { state: Arc>, } pub fn value_bytes(v: &OwnedValue) -> Option> { match v.deref() { Value::Array(a) => { let mut out = Vec::new(); for item in a.inner() { if let Value::U8(b) = item { out.push(*b); } else { return None; } } Some(out) } _ => None, } } /// Extract an array-of-strings (e.g. `ipv4.dns`, `802-1x.eap`) from a /// settings dict value. zvariant has no `TryFrom<&Value>` for `Vec`, /// so we peel the `Array` ourselves. pub fn value_str_list(v: &OwnedValue) -> Option> { match v.deref() { Value::Array(a) => { let mut out = Vec::new(); for item in a.inner() { match item { Value::Str(s) => out.push(s.as_str().to_string()), _ => return None, } } Some(out) } _ => None, } } fn ov(v: Value<'_>) -> OwnedValue { OwnedValue::try_from(v).expect("settings value is ownable") } /// OwnedValue isn't `Clone` (only `try_clone`), so settings dicts must be /// copied field-by-field when the fake hands one out. fn clone_settings(s: &SettingsMap) -> SettingsMap { s.iter() .map(|(section, vals)| { let cloned = vals .iter() .map(|(k, v)| v.try_clone().map(|c| (k.clone(), c))) .collect::, _>>() .expect("settings values are ownable"); (section.clone(), cloned) }) .collect() } fn conn_ssid(s: &SettingsMap) -> Option { s.get("802-11-wireless") .and_then(|m| m.get("ssid")) .and_then(value_bytes) .map(|b| String::from_utf8_lossy(&b).into_owned()) } fn conn_id(s: &SettingsMap) -> Option { s.get("connection") .and_then(|m| m.get("id")) .and_then(|v| v.downcast_ref::().ok()) } fn obj(path: &str) -> OwnedObjectPath { OwnedObjectPath::try_from(path).expect("valid object path") } // --------------------------------------------------------------------- // Root interface: org.freedesktop.NetworkManager // --------------------------------------------------------------------- struct FakeNm { shared: Shared, } #[interface(name = "org.freedesktop.NetworkManager")] impl FakeNm { async fn get_devices(&self) -> fdo::Result> { let st = self.shared.state.lock().unwrap(); Ok(st.devices.keys().map(|p| obj(p)).collect()) } #[zbus(property)] fn wireless_enabled(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().wireless_enabled) } // Setters must return `zbus::Error` (not `fdo::Error`): the macro's // generated setter keeps the fallible arm's error type verbatim, and the // dispatch future is typed `Result<(), zbus::Error>`. #[zbus(property)] fn set_wireless_enabled(&self, v: bool) -> zbus::Result<()> { self.shared.state.lock().unwrap().wireless_enabled = v; Ok(()) } #[zbus(property)] fn connectivity(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().connectivity) } async fn activate_connection( &self, conn: OwnedObjectPath, dev: OwnedObjectPath, _specific: OwnedObjectPath, #[zbus(connection)] c: &zbus::Connection, ) -> fdo::Result { let active = self.register_active(c, conn.as_str(), dev.as_str()).await?; Ok(active) } async fn add_and_activate_connection2( &self, settings: SettingsMap, dev: OwnedObjectPath, _specific: OwnedObjectPath, _options: HashMap, #[zbus(connection)] c: &zbus::Connection, ) -> fdo::Result<(OwnedObjectPath, OwnedObjectPath)> { let conn_path = self.save_connection(settings, c).await?; let active = self.register_active(c, conn_path.as_str(), dev.as_str()).await?; Ok((conn_path, active)) } } impl FakeNm { async fn save_connection(&self, settings: SettingsMap, c: &zbus::Connection) -> fdo::Result { let path = { let mut st = self.shared.state.lock().unwrap(); st.calls.push("AddAndActivateConnection2".into()); // Same UUID-exists semantics as real NetworkManager: this call // creates a *new* profile, so a duplicate UUID is an error, not // an upsert (updates go through Settings.Connection.Update2). if st.connections.values().any(|s| conn_id(s) == conn_id(&settings)) { return Err(fdo::Error::Failed( "A connection with this UUID already exists.".into(), )); } let p = format!("{CONNS_PREFIX}/{}", st.next_conn); st.next_conn += 1; st.connections.insert(p.clone(), settings); p }; c.object_server() .at(path.as_str(), FakeConn { shared: self.shared.clone(), path: path.clone(), }) .await?; Ok(obj(&path)) } async fn register_active( &self, c: &zbus::Connection, conn_path: &str, dev: &str, ) -> fdo::Result { let (active_path, new_ap) = { let mut st = self.shared.state.lock().unwrap(); st.calls.push(format!("activate {conn_path} -> {dev}")); if st.fail_next_activations > 0 { st.fail_next_activations -= 1; return Err(fdo::Error::Failed("transient activation failure".into())); } let ssid = st.connections.get(conn_path).and_then(conn_ssid); let target = st.land_on.clone().or(ssid); let mut dev_ap = target.as_ref().and_then(|t| { let aps = st.dev_aps.get(dev).cloned().unwrap_or_default(); aps.into_iter().find(|ap| { st.aps .get(ap) .map(|a| String::from_utf8_lossy(&a.ssid).into_owned() == *t) .unwrap_or(false) }) }); // Hidden networks don't appear in a scan until after they're // associated; create the AP on the fly in that case. let mut new_ap = None; if dev_ap.is_none() && st.connect_any { if let Some(t) = &target { let id = st.next_ap; st.next_ap += 1; let ap_path = format!("{APS_PREFIX}/{id}"); st.aps.insert( ap_path.clone(), FakeApData { ssid: t.as_bytes().to_vec(), strength: 80, flags: AP_FLAG_PRIVACY, wpa: 0, rsn: SEC_PSK, }, ); st.dev_aps.entry(dev.to_string()).or_default().push(ap_path.clone()); dev_ap = Some(ap_path.clone()); new_ap = Some(ap_path); } } if let Some(d) = st.devices.get_mut(dev) { if let Some(ap) = &dev_ap { d.active_ap = Some(ap.clone()); } d.state = DEV_STATE_ACTIVATED; } let p = format!("{ACTIVE_PREFIX}/{}", st.next_active); st.next_active += 1; st.active_conns.insert(p.clone(), conn_path.to_string()); st.dev_active.insert(dev.to_string(), p.clone()); (p, new_ap) }; if let Some(ap) = &new_ap { c.object_server() .at(ap.as_str(), FakeAp { shared: self.shared.clone(), path: ap.clone(), }) .await?; } c.object_server() .at(active_path.as_str(), FakeActive { shared: self.shared.clone(), path: active_path.clone(), }) .await?; Ok(obj(&active_path)) } } // --------------------------------------------------------------------- // Settings: org.freedesktop.NetworkManager.Settings // --------------------------------------------------------------------- struct FakeSettings { shared: Shared, } #[interface(name = "org.freedesktop.NetworkManager.Settings")] impl FakeSettings { async fn list_connections(&self) -> fdo::Result> { let st = self.shared.state.lock().unwrap(); Ok(st.connections.keys().map(|p| obj(p)).collect()) } async fn add_connection2( &self, settings: SettingsMap, _flags: u32, _args: HashMap, #[zbus(connection)] c: &zbus::Connection, ) -> fdo::Result<(OwnedObjectPath, HashMap)> { let path = { let mut st = self.shared.state.lock().unwrap(); st.calls.push("AddConnection2".into()); // Real NetworkManager rejects a duplicate UUID with // NM_SETTINGS_ERROR_UUID_EXISTS — it does NOT upsert. Existing // profiles must be edited via Settings.Connection.Update2; model // that here so a client regression fails loudly. if st.connections.values().any(|s| conn_id(s) == conn_id(&settings)) { return Err(fdo::Error::Failed( "A connection with this UUID already exists.".into(), )); } let p = format!("{CONNS_PREFIX}/{}", st.next_conn); st.next_conn += 1; st.connections.insert(p.clone(), settings); p }; c.object_server() .at(path.as_str(), FakeConn { shared: self.shared.clone(), path: path.clone(), }) .await?; Ok((obj(&path), HashMap::new())) } } // --------------------------------------------------------------------- // Settings.Connection // --------------------------------------------------------------------- struct FakeConn { shared: Shared, path: String, } #[interface(name = "org.freedesktop.NetworkManager.Settings.Connection")] impl FakeConn { async fn get_settings(&self) -> fdo::Result { let st = self.shared.state.lock().unwrap(); let settings = st .connections .get(&self.path) .ok_or_else(|| fdo::Error::UnknownObject(self.path.clone()))?; Ok(clone_settings(settings)) } async fn delete(&self) -> fdo::Result<()> { let mut st = self.shared.state.lock().unwrap(); st.calls.push(format!("delete {}", self.path)); st.connections.remove(&self.path); Ok(()) } async fn update2( &self, settings: SettingsMap, _flags: u32, _args: HashMap, ) -> fdo::Result> { let mut st = self.shared.state.lock().unwrap(); st.calls.push("Update2".into()); st.connections.insert(self.path.clone(), settings); Ok(HashMap::new()) } } // --------------------------------------------------------------------- // Access point // --------------------------------------------------------------------- struct FakeAp { shared: Shared, path: String, } #[interface(name = "org.freedesktop.NetworkManager.AccessPoint")] impl FakeAp { #[zbus(property)] fn ssid(&self) -> fdo::Result> { Ok(self.shared.state.lock().unwrap().aps[&self.path].ssid.clone()) } #[zbus(property)] fn strength(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().aps[&self.path].strength) } #[zbus(property)] fn flags(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().aps[&self.path].flags) } #[zbus(property)] fn wpa_flags(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().aps[&self.path].wpa) } #[zbus(property)] fn rsn_flags(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().aps[&self.path].rsn) } } // --------------------------------------------------------------------- // Device + Wireless + IP4Config // --------------------------------------------------------------------- struct FakeDevice { shared: Shared, path: String, } #[interface(name = "org.freedesktop.NetworkManager.Device")] impl FakeDevice { #[zbus(property)] fn interface(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().devices[&self.path].iface.clone()) } #[zbus(property)] fn device_type(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().devices[&self.path].dev_type) } #[zbus(property)] fn state(&self) -> fdo::Result { Ok(self.shared.state.lock().unwrap().devices[&self.path].state) } #[zbus(property)] fn active_connection(&self) -> fdo::Result { let st = self.shared.state.lock().unwrap(); Ok(st .dev_active .get(&self.path) .map(|p| obj(p)) .unwrap_or_else(|| obj("/"))) } #[zbus(property)] fn ip4_config(&self) -> fdo::Result { let st = self.shared.state.lock().unwrap(); Ok(obj(&st.devices[&self.path].ip4)) } async fn reapply( &self, _settings: SettingsMap, _version: u64, _flags: u32, ) -> fdo::Result<()> { let mut st = self.shared.state.lock().unwrap(); st.calls.push("Reapply".into()); Ok(()) } } struct FakeWireless { shared: Shared, path: String, } #[interface(name = "org.freedesktop.NetworkManager.Device.Wireless")] impl FakeWireless { #[zbus(property)] fn active_access_point(&self) -> fdo::Result { let st = self.shared.state.lock().unwrap(); Ok(st.devices[&self.path] .active_ap .as_ref() .map(|p| obj(p)) .unwrap_or_else(|| obj("/"))) } async fn get_all_access_points(&self) -> fdo::Result> { let st = self.shared.state.lock().unwrap(); Ok(st .dev_aps .get(&self.path) .map(|aps| aps.iter().map(|p| obj(p)).collect()) .unwrap_or_default()) } async fn request_scan(&self, _options: HashMap) -> fdo::Result<()> { let mut st = self.shared.state.lock().unwrap(); st.calls.push("RequestScan".into()); Ok(()) } } struct FakeIp4 { shared: Shared, /// Device path (so the fake can find the device's IP). dev: String, } #[interface(name = "org.freedesktop.NetworkManager.IP4Config")] impl FakeIp4 { #[zbus(property)] fn addresses(&self) -> fdo::Result> { let st = self.shared.state.lock().unwrap(); // A fixed, recognizable address: 192.168.1.42/24, gw .1. let _ = &st.devices[&self.dev]; Ok(vec![(0xC0A8012A, 24, 0xC0A80101)]) } } // --------------------------------------------------------------------- // Connection.Active // --------------------------------------------------------------------- struct FakeActive { shared: Shared, path: String, } #[interface(name = "org.freedesktop.NetworkManager.Connection.Active")] impl FakeActive { #[zbus(property)] fn connection(&self) -> fdo::Result { let st = self.shared.state.lock().unwrap(); Ok(obj(&st.active_conns[&self.path])) } } // --------------------------------------------------------------------- // Harness // --------------------------------------------------------------------- /// A private `dbus-daemon`. Dropping it kills the daemon. pub struct Daemon { pub addr: String, child: Child, } impl Drop for Daemon { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } static DBUS_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); /// Start a private `dbus-daemon` with permissive policies. Nothing is /// registered on it — attach the fake NM with [`serve_on`] if the test /// needs NetworkManager to be present. pub fn launch_daemon() -> Daemon { let n = DBUS_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst); let dir = std::env::temp_dir().join(format!( "breadcrumbs-dbus-{}-{}", std::process::id(), n )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create dbus dir"); let config = dir.join("bus.conf"); std::fs::write(&config, BUS_CONFIG).expect("write bus config"); let mut child = Command::new("dbus-daemon") .arg("--nofork") .arg("--nopidfile") .arg(format!("--config-file={}", config.display())) .arg("--print-address=1") .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .expect("dbus-daemon must be installed to run the NetworkManager fake tests"); let mut line = String::new(); std::io::BufReader::new(child.stdout.take().expect("dbus stdout")) .read_line(&mut line) .expect("read dbus-daemon address"); let addr = line .trim() .split(';') .next() .expect("address") .to_string(); Daemon { addr, child } } /// A fake NM service served on an existing (usually private) bus. pub struct FakeNmBus { pub addr: String, pub state: Arc>, server: zbus::blocking::Connection, } const BUS_CONFIG: &str = r#" session unix:tmpdir=/tmp "#; /// Serve the fake NetworkManager on the bus at `addr` and claim its name. /// Subprocesses pointed at the same address via `DBUS_SYSTEM_BUS_ADDRESS` /// will see this as their system NetworkManager. pub fn serve_on(addr: &str) -> FakeNmBus { let conn = zbus::blocking::connection::Builder::address(addr) .expect("connect to private bus") .build() .expect("build blocking connection"); let state = Arc::new(Mutex::new(FakeState { connectivity: 4, wireless_enabled: true, next_dev: 1, next_ap: 1, next_conn: 1, next_active: 1, ..Default::default() })); let shared = Shared { state: state.clone(), }; conn.object_server() .at(NM_PATH, FakeNm { shared: shared.clone(), }) .expect("register fake NM"); conn.object_server() .at(SETTINGS_PATH, FakeSettings { shared: shared.clone(), }) .expect("register fake settings"); conn.request_name(NM_DEST).expect("claim NM name"); FakeNmBus { addr: addr.to_string(), state, server: conn, } } impl FakeNmBus { pub fn addr(&self) -> &str { &self.addr } pub fn reset(&self) { let mut st = self.state.lock().unwrap(); st.devices.clear(); st.aps.clear(); st.dev_aps.clear(); st.connections.clear(); st.active_conns.clear(); st.dev_active.clear(); st.connectivity = 4; st.wireless_enabled = true; st.land_on = None; st.connect_any = false; st.fail_next_activations = 0; st.calls.clear(); st.next_dev = 1; st.next_ap = 1; st.next_conn = 1; st.next_active = 1; } /// Add a Wi-Fi device; returns its object path. `state` is the device /// state (e.g. 100 = ACTIVATED). pub fn add_wifi_device(&self, iface: &str, state: u32) -> String { let (dev_path, ip4_path) = { let mut st = self.state.lock().unwrap(); let id = st.next_dev; st.next_dev += 1; let dev_path = format!("{DEVICES_PREFIX}/{id}"); let ip4_path = format!("{IP4_PREFIX}/{id}"); st.devices.insert( dev_path.clone(), FakeDeviceData { iface: iface.to_string(), dev_type: DEV_TYPE_WIFI, state, active_ap: None, ip4: ip4_path.clone(), }, ); (dev_path, ip4_path) }; self.server .object_server() .at( dev_path.as_str(), FakeDevice { shared: self.shared(), path: dev_path.clone(), }, ) .expect("register device"); self.server .object_server() .at( dev_path.as_str(), FakeWireless { shared: self.shared(), path: dev_path.clone(), }, ) .expect("register wireless"); self.server .object_server() .at( ip4_path.as_str(), FakeIp4 { shared: self.shared(), dev: dev_path.clone(), }, ) .expect("register ip4"); dev_path } fn shared(&self) -> Shared { Shared { state: self.state.clone(), } } /// Add an access point to a device; returns its object path. pub fn add_ap(&self, dev: &str, ssid: &str, strength: u8, sec: Security) -> String { let (ap_path, flags, wpa, rsn) = { let mut st = self.state.lock().unwrap(); let id = st.next_ap; st.next_ap += 1; let ap_path = format!("{APS_PREFIX}/{id}"); let (flags, wpa, rsn) = sec.to_flags(); st.aps.insert( ap_path.clone(), FakeApData { ssid: ssid.as_bytes().to_vec(), strength, flags, wpa, rsn, }, ); st.dev_aps.entry(dev.to_string()).or_default().push(ap_path.clone()); (ap_path, flags, wpa, rsn) }; let _ = (flags, wpa, rsn); self.server .object_server() .at( ap_path.as_str(), FakeAp { shared: self.shared(), path: ap_path.clone(), }, ) .expect("register AP"); ap_path } pub fn set_active_ap(&self, dev: &str, ap: &str) { let mut st = self.state.lock().unwrap(); if let Some(d) = st.devices.get_mut(dev) { d.active_ap = Some(ap.to_string()); d.state = DEV_STATE_ACTIVATED; } } /// When set, any activation lands the device on the AP with this SSID /// (simulating an NM autoconnect race). pub fn set_land_on(&self, ssid: Option<&str>) { self.state.lock().unwrap().land_on = ssid.map(str::to_string); } /// When enabled, connecting to a network with no visible AP creates one /// (hidden-network semantics). pub fn set_connect_any(&self, on: bool) { self.state.lock().unwrap().connect_any = on; } /// Make the next `n` activation attempts fail (transient-failure /// simulation, e.g. for `init --wait` retries). pub fn fail_next_activations(&self, n: u32) { self.state.lock().unwrap().fail_next_activations = n; } pub fn set_connectivity(&self, c: u32) { self.state.lock().unwrap().connectivity = c; } pub fn set_device_state(&self, dev: &str, state: u32) { let mut st = self.state.lock().unwrap(); if let Some(d) = st.devices.get_mut(dev) { d.state = state; } } /// Save a wireless connection profile (as `nm` would create one) and /// return its path. pub fn save_connection(&self, ssid: &str, password: Option<&str>) -> String { let mut settings: SettingsMap = HashMap::new(); let mut conn: HashMap = HashMap::new(); conn.insert("id".into(), ov(Value::from(ssid.to_string()))); conn.insert("type".into(), ov(Value::from("802-11-wireless"))); conn.insert( "uuid".into(), ov(Value::from("00000000-0000-4000-8000-000000000001")), ); settings.insert("connection".into(), conn); let mut wifi: HashMap = HashMap::new(); wifi.insert("ssid".into(), ov(Value::from(ssid.as_bytes().to_vec()))); wifi.insert("mode".into(), ov(Value::from("infrastructure"))); settings.insert("802-11-wireless".into(), wifi); if let Some(pw) = password { let mut sec: HashMap = HashMap::new(); sec.insert("key-mgmt".into(), ov(Value::from("wpa-psk"))); sec.insert("psk".into(), ov(Value::from(pw.to_string()))); settings.insert("802-11-wireless-security".into(), sec); } let mut ipv4: HashMap = HashMap::new(); ipv4.insert("method".into(), ov(Value::from("auto"))); settings.insert("ipv4".into(), ipv4); let path = { let mut st = self.state.lock().unwrap(); st.calls.push(format!("save {ssid}")); let path = format!("{CONNS_PREFIX}/{}", st.next_conn); st.next_conn += 1; st.connections.insert(path.clone(), settings); path }; self.server .object_server() .at( path.as_str(), FakeConn { shared: self.shared(), path: path.clone(), }, ) .expect("register saved connection"); path } pub fn calls(&self) -> Vec { self.state.lock().unwrap().calls.clone() } pub fn connection_count(&self) -> usize { self.state.lock().unwrap().connections.len() } pub fn device_state(&self, dev: &str) -> u32 { self.state.lock().unwrap().devices.get(dev).map(|d| d.state).unwrap_or(0) } pub fn active_ssid(&self, dev: &str) -> Option { let st = self.state.lock().unwrap(); let ap = st.devices.get(dev)?.active_ap.clone()?; st.aps.get(&ap).map(|a| String::from_utf8_lossy(&a.ssid).into_owned()) } /// SSIDs of every connection activated so far, in activation order. pub fn activated_ssids(&self) -> Vec { let st = self.state.lock().unwrap(); st.active_conns .values() .filter_map(|p| st.connections.get(p).and_then(conn_ssid)) .collect() } } // --------------------------------------------------------------------- // Shared in-process bus (flow_watch tests) // --------------------------------------------------------------------- struct SharedBus { _daemon: Daemon, bus: FakeNmBus, } static SHARED: OnceLock> = OnceLock::new(); /// The process-wide fake NM bus for in-process tests. The env var /// `DBUS_SYSTEM_BUS_ADDRESS` is pointed at it once, so the production /// `nm` module (which uses `Connection::system()`) reaches it with zero /// test seams. Tests using this must serialize against each other — the /// returned guard holds the bus's lock for its whole lifetime. pub struct SharedNm { guard: MutexGuard<'static, SharedBus>, } impl std::ops::Deref for SharedNm { type Target = FakeNmBus; fn deref(&self) -> &FakeNmBus { &self.guard.bus } } pub fn shared() -> SharedNm { let bus = SHARED.get_or_init(|| { let daemon = launch_daemon(); let bus = serve_on(&daemon.addr); std::env::set_var("DBUS_SYSTEM_BUS_ADDRESS", &daemon.addr); Mutex::new(SharedBus { _daemon: daemon, bus }) }); let guard = bus.lock().unwrap_or_else(|e| e.into_inner()); SharedNm { guard } } /// Convenience guard used by tests that only need the bus available (no /// state control) — e.g. classify tests that merely observe "no adapter". /// Ensures the shared bus is up (and the env var set) before any `nm` /// call happens. pub fn ensure_shared() -> SharedNm { shared() }