1#[cfg(any(test, not(feature = "demo")))]
2use std::collections::HashMap;
3use std::error::Error;
4
5#[cfg(any(test, not(feature = "demo")))]
6use dbus::arg::{PropMap, RefArg, Variant};
7
8use crate::wifi::WifiNetwork;
9
10#[cfg(feature = "demo")]
11pub(crate) mod demo;
12#[cfg(not(feature = "demo"))]
13pub(crate) mod networkmanager;
14
15pub enum ConnectionRequest<'a> {
16 Open {
17 network: &'a WifiNetwork,
18 },
19 Secured {
20 network: &'a WifiNetwork,
21 passphrase: &'a str,
22 },
23}
24
25#[cfg(any(test, not(feature = "demo")))]
26fn variant<T: RefArg + 'static>(value: T) -> Variant<Box<dyn RefArg>> {
27 Variant(Box::new(value))
28}
29
30#[cfg(any(test, not(feature = "demo")))]
31fn base_connection_settings(ssid: &str) -> HashMap<&'static str, PropMap> {
32 let mut connection = PropMap::new();
33 connection
34 .insert("type".to_string(), variant("802-11-wireless".to_string()));
35 connection.insert("id".to_string(), variant(format!("nm-wifi-{ssid}")));
36
37 let mut wireless = PropMap::new();
38 wireless.insert("ssid".to_string(), variant(ssid.as_bytes().to_vec()));
39 wireless.insert("mode".to_string(), variant("infrastructure".to_string()));
40
41 let mut ipv4 = PropMap::new();
42 ipv4.insert("method".to_string(), variant("auto".to_string()));
43
44 let mut ipv6 = PropMap::new();
45 ipv6.insert("method".to_string(), variant("auto".to_string()));
46
47 let mut settings = HashMap::new();
48 settings.insert("connection", connection);
49 settings.insert("802-11-wireless", wireless);
50 settings.insert("ipv4", ipv4);
51 settings.insert("ipv6", ipv6);
52 settings
53}
54
55#[cfg(any(test, not(feature = "demo")))]
56fn open_network_connection_settings(
57 ssid: &str,
58) -> HashMap<&'static str, PropMap> {
59 base_connection_settings(ssid)
60}
61
62#[cfg(any(test, not(feature = "demo")))]
63fn secured_network_connection_settings(
64 ssid: &str,
65 password: &str,
66 key_mgmt: &str,
67) -> HashMap<&'static str, PropMap> {
68 let mut settings = base_connection_settings(ssid);
69
70 let mut wireless_security = PropMap::new();
71 wireless_security
72 .insert("key-mgmt".to_string(), variant(key_mgmt.to_string()));
73 wireless_security.insert("psk".to_string(), variant(password.to_string()));
74
75 if let Some(wireless) = settings.get_mut("802-11-wireless") {
76 wireless.insert(
77 "security".to_string(),
78 variant("802-11-wireless-security".to_string()),
79 );
80 }
81
82 settings.insert("802-11-wireless-security", wireless_security);
83 settings
84}
85
86#[cfg(feature = "demo")]
87pub use demo::demo_networks;
88
89#[cfg(feature = "demo")]
90pub fn get_connected_ssid() -> Result<Option<String>, Box<dyn Error>> {
91 demo::get_connected_ssid()
92}
93
94#[cfg(not(feature = "demo"))]
95pub fn get_connected_ssid() -> Result<Option<String>, Box<dyn Error>> {
96 networkmanager::get_connected_ssid()
97}
98
99#[cfg(feature = "demo")]
100pub fn get_wifi_adapter_name() -> Result<Option<String>, Box<dyn Error>> {
101 demo::get_wifi_adapter_name()
102}
103
104#[cfg(not(feature = "demo"))]
105pub fn get_wifi_adapter_name() -> Result<Option<String>, Box<dyn Error>> {
106 networkmanager::get_wifi_adapter_name()
107}
108
109#[cfg(feature = "demo")]
110pub async fn scan_wifi_networks() -> Result<Vec<WifiNetwork>, Box<dyn Error>> {
111 demo::scan_wifi_networks().await
112}
113
114#[cfg(not(feature = "demo"))]
115pub async fn scan_wifi_networks() -> Result<Vec<WifiNetwork>, Box<dyn Error>> {
116 networkmanager::scan_wifi_networks().await
117}
118
119#[cfg(feature = "demo")]
120pub fn connect_to_network(
121 request: ConnectionRequest<'_>,
122) -> Result<(), Box<dyn Error>> {
123 demo::connect_to_network(request)
124}
125
126#[cfg(not(feature = "demo"))]
127pub fn connect_to_network(
128 request: ConnectionRequest<'_>,
129) -> Result<(), Box<dyn Error>> {
130 networkmanager::connect_to_network(request)
131}
132
133#[cfg(feature = "demo")]
134pub fn disconnect_from_network(
135 network: &WifiNetwork,
136) -> Result<(), Box<dyn Error>> {
137 demo::disconnect_from_network(network)
138}
139
140#[cfg(not(feature = "demo"))]
141pub fn disconnect_from_network(
142 network: &WifiNetwork,
143) -> Result<(), Box<dyn Error>> {
144 networkmanager::disconnect_from_network(network)
145}
146
147#[cfg(test)]
148mod tests {
149 #[cfg(not(feature = "demo"))]
150 use std::time::Duration;
151
152 #[cfg(feature = "demo")]
153 use super::ConnectionRequest;
154 #[cfg(feature = "demo")]
155 use super::demo::{connect_to_network, demo_networks, scan_wifi_networks};
156 #[cfg(not(feature = "demo"))]
157 use super::networkmanager::{
158 AP_FLAGS_PRIVACY,
159 AP_SEC_KEY_MGMT_8021X,
160 AP_SEC_KEY_MGMT_PSK,
161 AP_SEC_KEY_MGMT_SAE,
162 SecurityKind,
163 choose_wifi_adapter_name,
164 classify_access_point_security,
165 classify_security,
166 scan_wait_duration,
167 should_disconnect_device,
168 };
169 use super::{
170 open_network_connection_settings,
171 secured_network_connection_settings,
172 };
173 #[cfg(not(feature = "demo"))]
174 use crate::wifi::WifiNetwork;
175 use crate::wifi::WifiSecurity;
176
177 #[cfg(not(feature = "demo"))]
178 #[test]
179 fn adapter_selection_prefers_connected_wifi_interfaces() {
180 assert_eq!(
181 choose_wifi_adapter_name(
182 Some("wlp2s0".to_string()),
183 vec!["wlan1".to_string(), "wlp2s0".to_string()]
184 ),
185 Some("wlp2s0".to_string())
186 );
187 }
188
189 #[cfg(not(feature = "demo"))]
190 #[test]
191 fn adapter_selection_falls_back_to_first_available_wifi_interface() {
192 assert_eq!(
193 choose_wifi_adapter_name(
194 None,
195 vec!["wlan1".to_string(), "wlp2s0".to_string()]
196 ),
197 Some("wlan1".to_string())
198 );
199 }
200
201 #[cfg(not(feature = "demo"))]
202 #[test]
203 fn disconnect_matching_requires_the_selected_ssid() {
204 assert!(should_disconnect_device(Some("home"), "home"));
205 assert!(!should_disconnect_device(Some("guest"), "home"));
206 assert!(!should_disconnect_device(None, "home"));
207 }
208
209 #[cfg(not(feature = "demo"))]
210 fn network(security: WifiSecurity) -> WifiNetwork {
211 WifiNetwork {
212 ssid: "test".to_string(),
213 signal_strength: 60,
214 security,
215 frequency: 2412,
216 connected: false,
217 }
218 }
219
220 #[cfg(not(feature = "demo"))]
221 #[test]
222 fn open_networks_are_classified_as_open() {
223 assert_eq!(
224 classify_security(&network(WifiSecurity::Open), None),
225 SecurityKind::Open
226 );
227 }
228
229 #[cfg(not(feature = "demo"))]
230 #[test]
231 fn access_points_with_psk_flags_are_classified_as_wpa_personal() {
232 assert_eq!(
233 classify_access_point_security(0, 0, AP_SEC_KEY_MGMT_PSK),
234 WifiSecurity::WpaPsk
235 );
236 }
237
238 #[cfg(not(feature = "demo"))]
239 #[test]
240 fn access_points_with_sae_flags_are_classified_as_wpa3_personal() {
241 assert_eq!(
242 classify_access_point_security(0, 0, AP_SEC_KEY_MGMT_SAE),
243 WifiSecurity::WpaSae
244 );
245 }
246
247 #[cfg(not(feature = "demo"))]
248 #[test]
249 fn enterprise_access_points_are_not_treated_as_personal_networks() {
250 assert_eq!(
251 classify_access_point_security(0, 0, AP_SEC_KEY_MGMT_8021X),
252 WifiSecurity::Enterprise
253 );
254 }
255
256 #[cfg(not(feature = "demo"))]
257 #[test]
258 fn privacy_without_supported_key_management_is_unsupported() {
259 assert_eq!(
260 classify_access_point_security(AP_FLAGS_PRIVACY, 0, 0),
261 WifiSecurity::Unsupported
262 );
263 }
264
265 #[test]
266 fn open_network_settings_include_wireless_and_ip_defaults() {
267 let settings = open_network_connection_settings("cafe");
268
269 assert!(settings.contains_key("connection"));
270 assert!(settings.contains_key("802-11-wireless"));
271 assert!(settings.contains_key("ipv4"));
272 assert!(settings.contains_key("ipv6"));
273 }
274
275 #[test]
276 fn psk_network_settings_include_wireless_security() {
277 let settings =
278 secured_network_connection_settings("home", "hunter2", "wpa-psk");
279
280 assert!(settings.contains_key("802-11-wireless-security"));
281 assert_eq!(
282 settings
283 .get("802-11-wireless")
284 .and_then(|wireless| wireless.get("security"))
285 .and_then(|value| value.0.as_str()),
286 Some("802-11-wireless-security")
287 );
288 assert_eq!(
289 settings
290 .get("802-11-wireless-security")
291 .and_then(|security| security.get("key-mgmt"))
292 .and_then(|value| value.0.as_str()),
293 Some("wpa-psk")
294 );
295 }
296
297 #[test]
298 fn sae_network_settings_use_sae_key_management() {
299 let settings =
300 secured_network_connection_settings("home", "hunter2", "sae");
301
302 assert_eq!(
303 settings
304 .get("802-11-wireless-security")
305 .and_then(|security| security.get("key-mgmt"))
306 .and_then(|value| value.0.as_str()),
307 Some("sae")
308 );
309 }
310
311 #[cfg(not(feature = "demo"))]
312 #[test]
313 fn psk_networks_are_classified_when_password_is_present() {
314 assert_eq!(
315 classify_security(&network(WifiSecurity::WpaPsk), Some("hunter2")),
316 SecurityKind::WpaPsk
317 );
318 }
319
320 #[cfg(not(feature = "demo"))]
321 #[test]
322 fn sae_networks_are_classified_when_password_is_present() {
323 assert_eq!(
324 classify_security(&network(WifiSecurity::WpaSae), Some("hunter2")),
325 SecurityKind::WpaSae
326 );
327 }
328
329 #[cfg(not(feature = "demo"))]
330 #[test]
331 fn enterprise_networks_remain_unsupported_even_with_a_password() {
332 assert_eq!(
333 classify_security(
334 &network(WifiSecurity::Enterprise),
335 Some("correcthorsebatterystaple")
336 ),
337 SecurityKind::Unsupported
338 );
339 }
340
341 #[cfg(not(feature = "demo"))]
342 #[test]
343 fn unsupported_connect_cases_are_detected() {
344 assert_eq!(
345 classify_security(&network(WifiSecurity::Unsupported), None),
346 SecurityKind::Unsupported
347 );
348 }
349
350 #[cfg(not(feature = "demo"))]
351 #[test]
352 fn recent_scans_do_not_force_an_extra_wait() {
353 assert_eq!(scan_wait_duration(5_000), Duration::from_millis(0));
354 }
355
356 #[cfg(not(feature = "demo"))]
357 #[test]
358 fn stale_scans_wait_longer_than_the_old_fixed_delay() {
359 assert_eq!(scan_wait_duration(20_000), Duration::from_millis(750));
360 assert_eq!(scan_wait_duration(-1), Duration::from_millis(750));
361 }
362
363 #[cfg(feature = "demo")]
364 #[tokio::test]
365 async fn demo_scan_returns_mock_networks() {
366 let networks = scan_wifi_networks().await.expect("demo scan works");
367 assert!(networks.iter().any(|network| network.ssid == "CatCat"));
368 assert!(
369 networks
370 .iter()
371 .any(|network| network.security == WifiSecurity::WpaSae)
372 );
373 }
374
375 #[cfg(feature = "demo")]
376 #[test]
377 fn demo_connect_accepts_matching_passwords() {
378 let network = demo_networks()
379 .into_iter()
380 .find(|network| network.ssid == "CatCat")
381 .expect("demo network exists");
382
383 let result = connect_to_network(ConnectionRequest::Secured {
384 network: &network,
385 passphrase: "AcerolaAcai",
386 });
387
388 assert!(result.is_ok());
389 }
390
391 #[cfg(feature = "demo")]
392 #[test]
393 fn demo_connect_rejects_invalid_passwords() {
394 let network = demo_networks()
395 .into_iter()
396 .find(|network| network.ssid == "CatCat")
397 .expect("demo network exists");
398
399 let result = connect_to_network(ConnectionRequest::Secured {
400 network: &network,
401 passphrase: "wrong-password",
402 });
403
404 assert_eq!(
405 result.expect_err("demo connect should fail").to_string(),
406 "Demo mode: invalid password"
407 );
408 }
409}