wlancfg_lib/config_management/
config_manager.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::network_config::{
6    ConnectFailure, Credential, FailureReason, HIDDEN_PROBABILITY_HIGH, HiddenProbEvent,
7    NetworkConfig, NetworkConfigError, NetworkIdentifier, PastConnectionData, PastConnectionList,
8    SecurityType,
9};
10use super::stash_conversion::*;
11use crate::client::types::{self, ScanObservation};
12use crate::telemetry::{TelemetryEvent, TelemetrySender};
13use anyhow::format_err;
14use async_trait::async_trait;
15use futures::lock::Mutex;
16use log::{error, info};
17use std::collections::hash_map::Entry;
18use std::collections::{HashMap, HashSet};
19use wlan_storage::policy::{POLICY_STORAGE_ID, PolicyStorage};
20use {
21    fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme,
22    fuchsia_async as fasync,
23};
24
25const MAX_CONFIGS_PER_SSID: usize = 1;
26
27/// The Saved Network Manager keeps track of saved networks and provides thread-safe access to
28/// saved networks. Networks are saved by NetworkConfig and accessed by their NetworkIdentifier
29/// (SSID and security protocol). Network configs are saved in-memory, and part of each network
30/// data is saved persistently. Futures aware locks are used in order to wait for the storage flush
31/// operations to complete when data changes.
32pub struct SavedNetworksManager {
33    saved_networks: Mutex<NetworkConfigMap>,
34    // Persistent storage for networks, which should be updated when there is a change to the data
35    // that is saved between reboots.
36    store: Mutex<PolicyStorage>,
37    telemetry_sender: TelemetrySender,
38}
39
40/// Save multiple network configs per SSID in able to store multiple connections with different
41/// credentials, for different authentication credentials on the same network or for different
42/// networks with the same name.
43type NetworkConfigMap = HashMap<NetworkIdentifier, Vec<NetworkConfig>>;
44
45#[async_trait(?Send)]
46pub trait SavedNetworksManagerApi {
47    /// Attempt to remove the NetworkConfig described by the specified NetworkIdentifier and
48    /// Credential. Return true if a NetworkConfig is remove and false otherwise.
49    async fn remove(
50        &self,
51        network_id: NetworkIdentifier,
52        credential: Credential,
53    ) -> Result<bool, NetworkConfigError>;
54
55    /// Get the count of networks in store, including multiple values with same SSID
56    async fn known_network_count(&self) -> usize;
57
58    /// Return a list of network configs that match the given SSID.
59    async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig>;
60
61    /// Return a list of network configs that could be used with the security type seen in a scan.
62    /// This includes configs that have a lower security type that can be upgraded to match the
63    /// provided detailed security type.
64    async fn lookup_compatible(
65        &self,
66        ssid: &types::Ssid,
67        scan_security: types::SecurityTypeDetailed,
68    ) -> Vec<NetworkConfig>;
69
70    /// Save a network by SSID and password. If the SSID and password have been saved together
71    /// before, do not modify the saved config. Update the legacy storage to keep it consistent
72    /// with what it did before the new version. If a network is pushed out because of the newly
73    /// saved network, this will return the removed config.
74    async fn store(
75        &self,
76        network_id: NetworkIdentifier,
77        credential: Credential,
78    ) -> Result<Option<NetworkConfig>, NetworkConfigError>;
79
80    /// Update the specified saved network with the result of an attempted connect.  If the
81    /// specified network could have been connected to with a different security type and we
82    /// do not find the specified config, we will check the other possible security type. For
83    /// example if a WPA3 network is specified, we will check WPA2 if it isn't found. If the
84    /// specified network is not saved, this function does not save it.
85    async fn record_connect_result(
86        &self,
87        id: NetworkIdentifier,
88        credential: &Credential,
89        bssid: types::Bssid,
90        connect_result: fidl_sme::ConnectResult,
91        scan_type: types::ScanObservation,
92    );
93
94    /// Record the disconnect from a network, to be used for things such as avoiding connections
95    /// that drop soon after starting.
96    async fn record_disconnect(
97        &self,
98        id: &NetworkIdentifier,
99        credential: &Credential,
100        data: PastConnectionData,
101    );
102
103    async fn record_periodic_metrics(&self);
104
105    /// Update hidden networks probabilities based on scan results. Record either results of a
106    /// passive scan or a directed active scan.
107    async fn record_scan_result(
108        &self,
109        target_ssids: Vec<types::Ssid>,
110        results: &HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>>,
111    );
112
113    async fn is_network_single_bss(
114        &self,
115        id: &NetworkIdentifier,
116        credential: &Credential,
117    ) -> Result<bool, anyhow::Error>;
118
119    // Return a list of every network config that has been saved.
120    async fn get_networks(&self) -> Vec<NetworkConfig>;
121
122    // Get the list of past connections for a specific BSS
123    async fn get_past_connections(
124        &self,
125        id: &NetworkIdentifier,
126        credential: &Credential,
127        bssid: &types::Bssid,
128    ) -> PastConnectionList;
129}
130
131impl SavedNetworksManager {
132    /// Initializes a new Saved Network Manager by reading saved networks from local storage using
133    /// a WLAN helper library. It will attempt to migrate any data from legacy storage.
134    pub async fn new(telemetry_sender: TelemetrySender) -> Self {
135        let storage = PolicyStorage::new_with_id(POLICY_STORAGE_ID).await;
136        Self::new_with_storage(storage, telemetry_sender).await
137    }
138
139    /// Load data from persistent storage. The legacy stash data is deleted if it exists.
140    pub async fn new_with_storage(
141        mut store: PolicyStorage,
142        telemetry_sender: TelemetrySender,
143    ) -> Self {
144        let mut saved_networks: HashMap<NetworkIdentifier, Vec<NetworkConfig>> = HashMap::new();
145        // Load saved networks from persistent storage. An error loading would mean that there was
146        // nothing saved in the current version of persistent store and there was an error loading
147        // legacy stash data.
148        let stored_networks = store.load().await.unwrap_or_else(|e| {
149            // If there is an error loading saved networks, we will run with no saved networks.
150            error!("No saved networks loaded; error loading saved networks from storage: {}", e);
151            Vec::new()
152        });
153        let mut errors_building_configs = HashSet::new();
154
155        // Collect the list of persisted networks into the map that will be used internally.
156        for persisted_data in stored_networks.into_iter() {
157            let id = NetworkIdentifier::new(
158                types::Ssid::from_bytes_unchecked(persisted_data.ssid),
159                persisted_data.security_type.into(),
160            );
161            let config = NetworkConfig::new(
162                id.clone(),
163                persisted_data.credential.clone().into(),
164                persisted_data.has_ever_connected,
165            );
166            match config {
167                Ok(config) => saved_networks.entry(id).or_default().push(config),
168                Err(e) => {
169                    _ = errors_building_configs.insert(e);
170                }
171            }
172        }
173
174        // If there errors creating network configs from persisted data, log unique types.
175        if !errors_building_configs.is_empty() {
176            error!(
177                "At least one error occurred building network config from persisted data: {:?}",
178                errors_building_configs
179            )
180        }
181
182        Self {
183            saved_networks: Mutex::new(saved_networks),
184            store: Mutex::new(store),
185            telemetry_sender,
186        }
187    }
188
189    /// Creates a new config with a random storage path, ensuring a clean environment for an
190    /// individual test
191    #[cfg(test)]
192    pub async fn new_for_test() -> Self {
193        use crate::util::testing::generate_string;
194        use futures::channel::mpsc;
195
196        let store_id = generate_string();
197        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
198        let telemetry_sender = TelemetrySender::new(telemetry_sender);
199        let store = PolicyStorage::new_with_id(&store_id).await;
200        Self::new_with_storage(store, telemetry_sender).await
201    }
202
203    /// Clear the in memory storage and the persistent storage.
204    #[cfg(test)]
205    pub async fn clear(&self) -> Result<(), anyhow::Error> {
206        self.saved_networks.lock().await.clear();
207        self.store.lock().await.clear()
208    }
209}
210
211#[async_trait(?Send)]
212impl SavedNetworksManagerApi for SavedNetworksManager {
213    async fn remove(
214        &self,
215        network_id: NetworkIdentifier,
216        credential: Credential,
217    ) -> Result<bool, NetworkConfigError> {
218        // Find any matching NetworkConfig and remove it.
219        let mut saved_networks = self.saved_networks.lock().await;
220        if let Some(network_configs) = saved_networks.get_mut(&network_id) {
221            let original_len = network_configs.len();
222            // Keep the configs that don't match provided NetworkIdentifier and Credential.
223            network_configs.retain(|cfg| cfg.credential != credential);
224            if original_len != network_configs.len() {
225                // If there was only one config with this ID before removing it, remove the ID.
226                if network_configs.is_empty() {
227                    _ = saved_networks.remove(&network_id);
228                }
229
230                // Update persistent storage
231                self.store
232                    .lock()
233                    .await
234                    .write(persistent_data_from_config_map(&saved_networks))
235                    .map_err(|e| {
236                        error!("error writing network to persistent storage: {}", e);
237                        NetworkConfigError::FileWriteError
238                    })?;
239
240                return Ok(true);
241            } else {
242                // Log whether there were any matching credential types without logging specific
243                // network data
244                let credential_types = network_configs
245                    .iter()
246                    .map(|nc| nc.credential.type_str())
247                    .collect::<HashSet<_>>();
248                if credential_types.contains(credential.type_str()) {
249                    info!("No matching network with the provided credential was found to remove.");
250                } else {
251                    info!(
252                        "No credential matching type {:?} found to remove for this network identifier. Help: found credential type(s): {:?}",
253                        credential.type_str(),
254                        credential_types
255                    );
256                }
257            }
258        } else {
259            // Check whether there is another network with the same SSID but different security
260            // type to remove.
261            let mut found_securities = SecurityType::list_variants();
262            found_securities.retain(|security| {
263                let id = NetworkIdentifier::new(network_id.ssid.clone(), *security);
264                saved_networks.contains_key(&id)
265            });
266            if found_securities.is_empty() {
267                info!("No network was found to remove with the provided SSID.");
268            } else {
269                info!(
270                    "No config to remove with security type {:?}. Help: found different config(s) for this SSID with security {:?}",
271                    network_id.security_type, found_securities
272                );
273            }
274        }
275        Ok(false)
276    }
277
278    /// Get the count of networks in store, including multiple values with same SSID
279    async fn known_network_count(&self) -> usize {
280        self.saved_networks.lock().await.values().flatten().count()
281    }
282
283    /// Return the network configs that have this network identifier. The configs may be different
284    /// because of their credentials. Note that these are copies of the current data, so if data
285    /// could have changed it should be looked up again. For example, data about roam scans change
286    /// throughout a connection so callers cannot keep using the same network config for that data
287    /// throughout the connection.
288    async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig> {
289        self.saved_networks.lock().await.get(id).cloned().unwrap_or_default()
290    }
291
292    async fn lookup_compatible(
293        &self,
294        ssid: &types::Ssid,
295        scan_security: types::SecurityTypeDetailed,
296    ) -> Vec<NetworkConfig> {
297        let saved_networks_guard = self.saved_networks.lock().await;
298        let mut matching_configs = Vec::new();
299        for security in compatible_policy_securities(&scan_security) {
300            let id = NetworkIdentifier::new(ssid.clone(), security);
301            let saved_configs = saved_networks_guard.get(&id);
302            if let Some(configs) = saved_configs {
303                matching_configs.extend(
304                    configs
305                        .iter()
306                        // Check for conflicts; PSKs can't be used to connect to WPA3 networks.
307                        .filter(|config| security_is_compatible(&scan_security, &config.credential))
308                        .map(Clone::clone),
309                );
310            }
311        }
312        matching_configs
313    }
314
315    async fn store(
316        &self,
317        network_id: NetworkIdentifier,
318        credential: Credential,
319    ) -> Result<Option<NetworkConfig>, NetworkConfigError> {
320        let mut saved_networks = self.saved_networks.lock().await;
321        let network_entry = saved_networks.entry(network_id.clone());
322
323        if let Entry::Occupied(network_configs) = &network_entry
324            && network_configs.get().iter().any(|cfg| cfg.credential == credential)
325        {
326            info!("Saving a previously saved network with same password.");
327            return Ok(None);
328        }
329        let network_config = NetworkConfig::new(network_id.clone(), credential.clone(), false)?;
330        let network_configs = network_entry.or_default();
331        let evicted_config = evict_if_needed(network_configs);
332        network_configs.push(network_config);
333
334        self.store.lock().await.write(persistent_data_from_config_map(&saved_networks)).map_err(
335            |e| {
336                error!("error writing network to persistent storage: {}", e);
337                NetworkConfigError::FileWriteError
338            },
339        )?;
340
341        Ok(evicted_config)
342    }
343
344    async fn record_connect_result(
345        &self,
346        id: NetworkIdentifier,
347        credential: &Credential,
348        bssid: types::Bssid,
349        connect_result: fidl_sme::ConnectResult,
350        scan_type: types::ScanObservation,
351    ) {
352        let mut saved_networks = self.saved_networks.lock().await;
353        let networks = match saved_networks.get_mut(&id) {
354            Some(networks) => networks,
355            None => {
356                error!("Failed to find network to record result of connect attempt.");
357                return;
358            }
359        };
360        for network in networks.iter_mut() {
361            if &network.credential == credential {
362                match (connect_result.code, connect_result.is_credential_rejected) {
363                    (fidl_ieee80211::StatusCode::Success, _) => {
364                        let mut has_change = false;
365                        if !network.has_ever_connected {
366                            network.has_ever_connected = true;
367                            has_change = true;
368                        }
369                        // Update hidden network probabiltiy
370                        match scan_type {
371                            types::ScanObservation::Passive => {
372                                network.update_hidden_prob(HiddenProbEvent::ConnectPassive);
373                            }
374                            types::ScanObservation::Active => {
375                                network.update_hidden_prob(HiddenProbEvent::ConnectActive);
376                            }
377                            types::ScanObservation::Unknown => {}
378                        };
379
380                        if has_change {
381                            // Update persistent storage since a config has changed.
382                            let data = persistent_data_from_config_map(&saved_networks);
383                            if let Err(e) = self.store.lock().await.write(data) {
384                                info!("Failed to record successful connect in store: {}", e);
385                            }
386                        }
387                    }
388                    (fidl_ieee80211::StatusCode::Canceled, _) => {}
389                    (_, true) => {
390                        network.perf_stats.connect_failures.add(
391                            bssid,
392                            ConnectFailure {
393                                time: fasync::MonotonicInstant::now(),
394                                reason: FailureReason::CredentialRejected,
395                                bssid,
396                            },
397                        );
398                    }
399                    (_, _) => {
400                        network.perf_stats.connect_failures.add(
401                            bssid,
402                            ConnectFailure {
403                                time: fasync::MonotonicInstant::now(),
404                                reason: FailureReason::GeneralFailure,
405                                bssid,
406                            },
407                        );
408                    }
409                }
410                return;
411            }
412        }
413        // Will not reach here if we find the saved network with matching SSID and credential.
414        error!("Failed to find matching network to record result of connect attempt.");
415    }
416
417    async fn record_disconnect(
418        &self,
419        id: &NetworkIdentifier,
420        credential: &Credential,
421        data: PastConnectionData,
422    ) {
423        let bssid = data.bssid;
424        let mut saved_networks = self.saved_networks.lock().await;
425        let networks = match saved_networks.get_mut(id) {
426            Some(networks) => networks,
427            None => {
428                info!("Failed to find network to record disconnect stats");
429                return;
430            }
431        };
432        for network in networks.iter_mut() {
433            if &network.credential == credential {
434                network.perf_stats.past_connections.add(bssid, data);
435                return;
436            }
437        }
438    }
439
440    async fn record_periodic_metrics(&self) {
441        let saved_networks = self.saved_networks.lock().await;
442        // Count the number of configs for each saved network
443        let config_counts = saved_networks
444            .iter()
445            .map(|saved_network| {
446                let configs = saved_network.1;
447                configs.len()
448            })
449            .collect();
450        self.telemetry_sender.send(TelemetryEvent::SavedNetworkCount {
451            saved_network_count: saved_networks.len(),
452            config_count_per_saved_network: config_counts,
453        });
454    }
455
456    async fn record_scan_result(
457        &self,
458        target_ssids: Vec<types::Ssid>,
459        results: &HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>>,
460    ) {
461        let mut saved_networks = self.saved_networks.lock().await;
462
463        for (network, bss_list) in results {
464            // If there are BSSs seen with the same SSID but different security, it will be
465            // recorded as multi BSS. But this is fine since the network will just not get the
466            //  improvement to scan less.
467            let has_multiple_bss = bss_list.len() > 1;
468            // Determine if any BSSs seen for this network were observed passively.
469            if bss_list.iter().any(|bss| bss.observation == ScanObservation::Passive) {
470                // Look for compatible configs and record them as "SeenPassive" and with single
471                // or multi BSS data.
472                for security in compatible_policy_securities(&network.security_type) {
473                    let configs = match saved_networks
474                        .get_mut(&NetworkIdentifier::new(network.ssid.clone(), security))
475                    {
476                        Some(configs) => configs,
477                        None => continue,
478                    };
479                    // Check that the credential is compatible with the actual security type of
480                    // the scan result.
481                    let compatible_configs = configs.iter_mut().filter(|config| {
482                        security_is_compatible(&network.security_type, &config.credential)
483                    });
484                    for config in compatible_configs {
485                        config.update_hidden_prob(HiddenProbEvent::SeenPassive);
486                        config.update_seen_multiple_bss(has_multiple_bss)
487                    }
488                }
489            }
490        }
491
492        // Update saved networks that match one of the targeted SSIDs but were *not* in scan results.
493        for (id, configs) in saved_networks.iter_mut() {
494            if !target_ssids.contains(&id.ssid) {
495                continue;
496            }
497            // For each config, check whether there is a scan result that
498            // could be used to connect. If not, update the hidden probability.
499            let potential_scan_results =
500                results.iter().filter(|(scan_id, _)| scan_id.ssid == id.ssid).collect::<Vec<_>>();
501            for config in configs {
502                if !potential_scan_results.iter().any(|(scan_id, _)| {
503                    compatible_policy_securities(&scan_id.security_type)
504                        .contains(&config.security_type)
505                        && security_is_compatible(&scan_id.security_type, &config.credential)
506                }) {
507                    config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
508                }
509            }
510        }
511        // TODO(60619): Update persistent storage with new probability if it has changed
512    }
513
514    /// Returns whether or not the network likely has only one BSS based on previous scans. This
515    /// should be used instead of the network config if the network config may have been updated.
516    /// For example, when making roam scan decisions this should be used instead of a network
517    /// config obtained at the time of connecting.
518    async fn is_network_single_bss(
519        &self,
520        id: &NetworkIdentifier,
521        credential: &Credential,
522    ) -> Result<bool, anyhow::Error> {
523        let saved_networks_guard = self.saved_networks.lock().await;
524        let possible_configs = saved_networks_guard.get(id).ok_or_else(|| {
525            format_err!(
526                "error checking if network is single BSS; no config with matching identifier"
527            )
528        })?;
529        let config =
530            possible_configs.iter().find(|c| &c.credential == credential).ok_or_else(|| {
531                format_err!(
532                    "error checking if network is single BSS; no config with matching credential"
533                )
534            })?;
535        return Ok(config.is_likely_single_bss());
536    }
537
538    async fn get_networks(&self) -> Vec<NetworkConfig> {
539        self.saved_networks.lock().await.values().flat_map(|cfgs| cfgs.clone()).collect()
540    }
541
542    async fn get_past_connections(
543        &self,
544        id: &NetworkIdentifier,
545        credential: &Credential,
546        bssid: &types::Bssid,
547    ) -> PastConnectionList {
548        self.saved_networks
549            .lock()
550            .await
551            .get(id)
552            .and_then(|configs| configs.iter().find(|config| &config.credential == credential))
553            .map(|config| config.perf_stats.past_connections.get_list_for_bss(bssid))
554            .unwrap_or_default()
555    }
556}
557
558/// Returns a subset of potentially hidden saved networks, filtering probabilistically based
559/// on how certain they are to be hidden.
560pub fn select_subset_potentially_hidden_networks(
561    saved_networks: Vec<NetworkConfig>,
562) -> Vec<types::NetworkIdentifier> {
563    saved_networks
564        .into_iter()
565        .filter(|saved_network| {
566            // Roll a dice to see if we should scan for it. The function gen_range(low..high)
567            // has an inclusive lower bound and exclusive upper bound, so using it as
568            // `hidden_probability > gen_range(0..1)` means that:
569            // - hidden_probability of 1 will _always_ be selected
570            // - hidden_probability of 0 will _never_ be selected
571            saved_network.hidden_probability > rand::random_range(0.0..1.0)
572        })
573        .map(|network| types::NetworkIdentifier {
574            ssid: network.ssid,
575            security_type: network.security_type,
576        })
577        .collect()
578}
579
580/// Returns all saved networks which we think have a high probability of being hidden.
581pub fn select_high_probability_hidden_networks(
582    saved_networks: Vec<NetworkConfig>,
583) -> Vec<types::NetworkIdentifier> {
584    saved_networks
585        .into_iter()
586        .filter(|saved_network| saved_network.hidden_probability >= HIDDEN_PROBABILITY_HIGH)
587        .map(|network| types::NetworkIdentifier {
588            ssid: network.ssid,
589            security_type: network.security_type,
590        })
591        .collect()
592}
593
594/// Gets compatible `SecurityType`s for network candidates.
595///
596/// This function returns a sequence of `SecurityType`s that may be used to connect to a network
597/// configured as described by the given `SecurityTypeDetailed`. If there is no compatible
598/// `SecurityType`, then the sequence will be empty.
599pub fn compatible_policy_securities(
600    detailed_security: &types::SecurityTypeDetailed,
601) -> Vec<SecurityType> {
602    use fidl_sme::Protection::*;
603    match detailed_security {
604        Wpa3Enterprise | Wpa3Personal | Wpa2Wpa3Personal => {
605            vec![SecurityType::Wpa2, SecurityType::Wpa3]
606        }
607        Wpa2Enterprise
608        | Wpa2Personal
609        | Wpa1Wpa2Personal
610        | Wpa2PersonalTkipOnly
611        | Wpa1Wpa2PersonalTkipOnly => vec![SecurityType::Wpa, SecurityType::Wpa2],
612        Wpa1 => vec![SecurityType::Wpa],
613        Wep => vec![SecurityType::Wep],
614        Open => vec![SecurityType::None],
615        Unknown => vec![],
616    }
617}
618
619pub fn security_is_compatible(
620    scan_security: &types::SecurityTypeDetailed,
621    credential: &Credential,
622) -> bool {
623    if (scan_security == &types::SecurityTypeDetailed::Wpa3Personal
624        || scan_security == &types::SecurityTypeDetailed::Wpa3Enterprise)
625        && let Credential::Psk(_) = credential
626    {
627        return false;
628    }
629    true
630}
631
632/// If the list of configs is at capacity for the number of saved configs per SSID,
633/// remove a saved network that has never been successfully connected to. If all have
634/// been successfully connected to, remove any. If a network config is evicted, that connection
635/// is forgotten for future connections.
636/// TODO(https://fxbug.dev/42117293) - when network configs record information about successful connections,
637/// use this to make a better decision what to forget if all networks have connected before.
638/// TODO(https://fxbug.dev/42117730) - make sure that we disconnect from the network if we evict a network config
639/// for a network we are currently connected to.
640fn evict_if_needed(configs: &mut Vec<NetworkConfig>) -> Option<NetworkConfig> {
641    if configs.len() < MAX_CONFIGS_PER_SSID {
642        return None;
643    }
644
645    for i in 0..configs.len() {
646        if let Some(config) = configs.get(i)
647            && !config.has_ever_connected
648        {
649            return Some(configs.remove(i));
650        }
651    }
652    // If all saved networks have connected, remove the first network
653    Some(configs.remove(0))
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use crate::config_management::{
660        HistoricalListsByBssid, PROB_HIDDEN_DEFAULT, PROB_HIDDEN_IF_CONNECT_ACTIVE,
661        PROB_HIDDEN_IF_CONNECT_PASSIVE, PROB_HIDDEN_IF_SEEN_PASSIVE,
662    };
663    use crate::util::testing::{generate_random_bss, generate_string, random_connection_data};
664    use assert_matches::assert_matches;
665    use futures::channel::mpsc;
666    use futures::task::Poll;
667    use std::pin::pin;
668    use test_case::test_case;
669
670    #[fuchsia::test]
671    async fn store_and_lookup() {
672        let store_id = generate_string();
673        let saved_networks = create_saved_networks(&store_id).await;
674        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
675
676        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
677        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
678        assert_eq!(0, saved_networks.known_network_count().await);
679
680        // Store a network and verify it was stored.
681        assert!(
682            saved_networks
683                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
684                .await
685                .expect("storing 'foo' failed")
686                .is_none()
687        );
688        assert_eq!(
689            vec![network_config("foo", "qwertyuio")],
690            saved_networks.lookup(&network_id_foo).await
691        );
692        assert_eq!(1, saved_networks.known_network_count().await);
693
694        // Store another network with the same SSID.
695        let popped_network = saved_networks
696            .store(network_id_foo.clone(), Credential::Password(b"12345678".to_vec()))
697            .await
698            .expect("storing 'foo' a second time failed");
699        assert_eq!(popped_network, Some(network_config("foo", "qwertyuio")));
700
701        // There should only be one saved "foo" network because MAX_CONFIGS_PER_SSID is 1.
702        // When this constant becomes greater than 1, both network configs should be found
703        assert_eq!(
704            vec![network_config("foo", "12345678")],
705            saved_networks.lookup(&network_id_foo).await
706        );
707        assert_eq!(1, saved_networks.known_network_count().await);
708
709        // Store another network and verify.
710        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
711        let psk = Credential::Psk(vec![1; 32]);
712        let config_baz = NetworkConfig::new(network_id_baz.clone(), psk.clone(), false)
713            .expect("failed to create network config");
714        assert!(
715            saved_networks
716                .store(network_id_baz.clone(), psk)
717                .await
718                .expect("storing 'baz' with PSK failed")
719                .is_none()
720        );
721        assert_eq!(vec![config_baz.clone()], saved_networks.lookup(&network_id_baz).await);
722        assert_eq!(2, saved_networks.known_network_count().await);
723
724        // Saved networks should persist when we create a saved networks manager with the same ID.
725        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
726        let store = PolicyStorage::new_with_id(&store_id).await;
727
728        let saved_networks =
729            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
730                .await;
731        assert_eq!(
732            vec![network_config("foo", "12345678")],
733            saved_networks.lookup(&network_id_foo).await
734        );
735        assert_eq!(vec![config_baz], saved_networks.lookup(&network_id_baz).await);
736        assert_eq!(2, saved_networks.known_network_count().await);
737    }
738
739    #[fuchsia::test]
740    async fn store_twice() {
741        let saved_networks = SavedNetworksManager::new_for_test().await;
742        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
743
744        assert!(
745            saved_networks
746                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
747                .await
748                .expect("storing 'foo' failed")
749                .is_none()
750        );
751        let popped_network = saved_networks
752            .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
753            .await
754            .expect("storing 'foo' a second time failed");
755        // Because the same network was stored twice, nothing was evicted, so popped_network == None
756        assert_eq!(popped_network, None);
757        let expected_cfgs = vec![network_config("foo", "qwertyuio")];
758        assert_eq!(expected_cfgs, saved_networks.lookup(&network_id).await);
759        assert_eq!(1, saved_networks.known_network_count().await);
760    }
761
762    #[fuchsia::test]
763    async fn store_many_same_ssid() {
764        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
765        let saved_networks = SavedNetworksManager::new_for_test().await;
766
767        // save max + 1 networks with same SSID and different credentials
768        for i in 0..MAX_CONFIGS_PER_SSID + 1 {
769            let mut password = b"password".to_vec();
770            password.push(i as u8);
771            let popped_network = saved_networks
772                .store(network_id.clone(), Credential::Password(password))
773                .await
774                .expect("Failed to saved network");
775            if i >= MAX_CONFIGS_PER_SSID {
776                assert!(popped_network.is_some());
777            } else {
778                assert!(popped_network.is_none());
779            }
780        }
781
782        // since none have been connected to yet, we don't care which config was removed
783        assert_eq!(MAX_CONFIGS_PER_SSID, saved_networks.lookup(&network_id).await.len());
784    }
785
786    #[fuchsia::test]
787    async fn store_and_remove() {
788        let store_id = generate_string();
789        let saved_networks = create_saved_networks(&store_id).await;
790
791        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
792        let credential = Credential::Password(b"qwertyuio".to_vec());
793        assert!(saved_networks.lookup(&network_id).await.is_empty());
794        assert_eq!(0, saved_networks.known_network_count().await);
795
796        // Store a network and verify it was stored.
797        assert!(
798            saved_networks
799                .store(network_id.clone(), credential.clone())
800                .await
801                .expect("storing 'foo' failed")
802                .is_none()
803        );
804        assert_eq!(
805            vec![network_config("foo", "qwertyuio")],
806            saved_networks.lookup(&network_id).await
807        );
808        assert_eq!(1, saved_networks.known_network_count().await);
809
810        // Remove a network with the same NetworkIdentifier but differenct credential and verify
811        // that the saved network is unaffected.
812        assert!(
813            !saved_networks
814                .remove(network_id.clone(), Credential::Password(b"diff-password".to_vec()))
815                .await
816                .expect("removing 'foo' failed")
817        );
818        assert_eq!(1, saved_networks.known_network_count().await);
819
820        // Remove the network and check it is gone
821        assert!(
822            saved_networks
823                .remove(network_id.clone(), credential.clone())
824                .await
825                .expect("removing 'foo' failed")
826        );
827        assert_eq!(0, saved_networks.known_network_count().await);
828        // Check that the key in the saved networks manager's internal hashmap was removed.
829        assert!(saved_networks.saved_networks.lock().await.get(&network_id).is_none());
830
831        // If we try to remove the network again, we won't get an error and nothing happens
832        assert!(
833            !saved_networks
834                .remove(network_id.clone(), credential)
835                .await
836                .expect("removing 'foo' failed")
837        );
838
839        // Check that removal persists.
840        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
841        let store = PolicyStorage::new_with_id(&store_id).await;
842        let saved_networks =
843            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
844                .await;
845        assert_eq!(0, saved_networks.known_network_count().await);
846        assert!(saved_networks.lookup(&network_id).await.is_empty());
847    }
848
849    #[fuchsia::test]
850    fn sme_protection_converts_to_lower_compatible() {
851        use fidl_sme::Protection::*;
852        let lower_compatible_pairs = vec![
853            (Wpa3Enterprise, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
854            (Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
855            (Wpa2Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
856            (Wpa2Enterprise, vec![SecurityType::Wpa, SecurityType::Wpa2]),
857            (Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
858            (Wpa1Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
859            (Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
860            (Wpa1Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
861            (Wpa1, vec![SecurityType::Wpa]),
862            (Wep, vec![SecurityType::Wep]),
863            (Open, vec![SecurityType::None]),
864            (Unknown, vec![]),
865        ];
866        for (detailed_security, security) in lower_compatible_pairs {
867            assert_eq!(compatible_policy_securities(&detailed_security), security);
868        }
869    }
870
871    #[fuchsia::test]
872    async fn lookup_compatible_returns_both_compatible_configs() {
873        let saved_networks = SavedNetworksManager::new_for_test().await;
874        let ssid = types::Ssid::try_from("foo").unwrap();
875        let network_id_wpa2 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
876        let network_id_wpa3 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
877        let credential_wpa2 = Credential::Password(b"password".to_vec());
878        let credential_wpa3 = Credential::Password(b"wpa3-password".to_vec());
879
880        // Check that lookup_compatible does not modify the SavedNetworksManager and returns an
881        // empty vector if there is no matching config.
882        let results = saved_networks
883            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
884            .await;
885        assert!(results.is_empty());
886        assert_eq!(saved_networks.known_network_count().await, 0);
887
888        // Store a couple of network configs that could both be use to connect to a WPA2/WPA3
889        // network.
890        assert!(
891            saved_networks
892                .store(network_id_wpa2.clone(), credential_wpa2.clone())
893                .await
894                .expect("Failed to store network")
895                .is_none()
896        );
897        assert!(
898            saved_networks
899                .store(network_id_wpa3.clone(), credential_wpa3.clone())
900                .await
901                .expect("Failed to store network")
902                .is_none()
903        );
904        // Store a network with the same SSID but a not-compatible security type.
905        let network_id_wep = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa);
906        assert!(
907            saved_networks
908                .store(network_id_wep.clone(), Credential::Password(b"abcdefgh".to_vec()))
909                .await
910                .expect("Failed to store network")
911                .is_none()
912        );
913
914        let results = saved_networks
915            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
916            .await;
917        let expected_config_wpa2 = NetworkConfig::new(network_id_wpa2, credential_wpa2, false)
918            .expect("Failed to create config");
919        let expected_config_wpa3 = NetworkConfig::new(network_id_wpa3, credential_wpa3, false)
920            .expect("Failed to create config");
921        assert_eq!(results.len(), 2);
922        assert!(results.contains(&expected_config_wpa2));
923        assert!(results.contains(&expected_config_wpa3));
924    }
925
926    #[test_case(types::SecurityTypeDetailed::Wpa3Personal)]
927    #[test_case(types::SecurityTypeDetailed::Wpa3Enterprise)]
928    #[fuchsia::test(add_test_attr = false)]
929    fn lookup_compatible_does_not_return_wpa3_psk(
930        wpa3_detailed_security: types::SecurityTypeDetailed,
931    ) {
932        let mut exec = fasync::TestExecutor::new();
933        let saved_networks = exec.run_singlethreaded(SavedNetworksManager::new_for_test());
934
935        // Store a WPA3 config with a password that will match and a PSK config that won't match
936        // to a WPA3 network.
937        let ssid = types::Ssid::try_from("foo").unwrap();
938        let network_id_psk = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
939        let network_id_password = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
940        let credential_psk = Credential::Psk(vec![5; 32]);
941        let credential_password = Credential::Password(b"mypassword".to_vec());
942        assert!(
943            exec.run_singlethreaded(
944                saved_networks.store(network_id_psk.clone(), credential_psk.clone()),
945            )
946            .expect("Failed to store network")
947            .is_none()
948        );
949        assert!(
950            exec.run_singlethreaded(
951                saved_networks.store(network_id_password.clone(), credential_password.clone()),
952            )
953            .expect("Failed to store network")
954            .is_none()
955        );
956
957        // Only the WPA3 config with a credential should be returned.
958        let expected_config_wpa3 =
959            NetworkConfig::new(network_id_password, credential_password, false)
960                .expect("Failed to create configc");
961        let results = exec
962            .run_singlethreaded(saved_networks.lookup_compatible(&ssid, wpa3_detailed_security));
963        assert_eq!(results, vec![expected_config_wpa3]);
964    }
965
966    #[fuchsia::test]
967    async fn connect_network() {
968        let store_id = generate_string();
969
970        let saved_networks = create_saved_networks(&store_id).await;
971
972        let network_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
973        let credential = Credential::Password(b"password".to_vec());
974        let bssid = types::Bssid::from([4; 6]);
975
976        // If connect and network hasn't been saved, we should not save the network.
977        saved_networks
978            .record_connect_result(
979                network_id.clone(),
980                &credential,
981                bssid,
982                fake_successful_connect_result(),
983                types::ScanObservation::Unknown,
984            )
985            .await;
986        assert!(saved_networks.lookup(&network_id).await.is_empty());
987        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
988        assert_eq!(0, saved_networks.known_network_count().await);
989
990        // Save the network and record a successful connection.
991        assert!(
992            saved_networks
993                .store(network_id.clone(), credential.clone())
994                .await
995                .expect("Failed save network")
996                .is_none()
997        );
998
999        let config = network_config("bar", "password");
1000        assert_eq!(vec![config], saved_networks.lookup(&network_id).await);
1001
1002        saved_networks
1003            .record_connect_result(
1004                network_id.clone(),
1005                &credential,
1006                bssid,
1007                fake_successful_connect_result(),
1008                types::ScanObservation::Unknown,
1009            )
1010            .await;
1011
1012        // The network should be saved with the connection recorded. We should not have recorded
1013        // that the network was connected to passively or actively.
1014        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1015            assert!(config.has_ever_connected);
1016            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1017        });
1018
1019        saved_networks
1020            .record_connect_result(
1021                network_id.clone(),
1022                &credential,
1023                bssid,
1024                fake_successful_connect_result(),
1025                types::ScanObservation::Active,
1026            )
1027            .await;
1028        // We should now see that we connected to the network after an active scan.
1029        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1030            assert!(config.has_ever_connected);
1031            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1032        });
1033
1034        saved_networks
1035            .record_connect_result(
1036                network_id.clone(),
1037                &credential,
1038                bssid,
1039                fake_successful_connect_result(),
1040                types::ScanObservation::Passive,
1041            )
1042            .await;
1043        // The config should have a lower hidden probability after connecting after a passive scan.
1044        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1045            assert!(config.has_ever_connected);
1046            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1047        });
1048
1049        // Success connects should be saved as persistent data.
1050        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1051        let store = PolicyStorage::new_with_id(&store_id).await;
1052        let saved_networks =
1053            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1054                .await;
1055        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1056            assert!(config.has_ever_connected);
1057        });
1058    }
1059
1060    #[fuchsia::test]
1061    async fn test_record_connect_updates_one() {
1062        let saved_networks = SavedNetworksManager::new_for_test().await;
1063        let net_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1064        let net_id_also_valid = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
1065        let credential = Credential::Password(b"some_password".to_vec());
1066        let bssid = types::Bssid::from([2; 6]);
1067
1068        // Save the networks and record a successful connection.
1069        assert!(
1070            saved_networks
1071                .store(net_id.clone(), credential.clone())
1072                .await
1073                .expect("Failed save network")
1074                .is_none()
1075        );
1076        assert!(
1077            saved_networks
1078                .store(net_id_also_valid.clone(), credential.clone())
1079                .await
1080                .expect("Failed save network")
1081                .is_none()
1082        );
1083        saved_networks
1084            .record_connect_result(
1085                net_id.clone(),
1086                &credential,
1087                bssid,
1088                fake_successful_connect_result(),
1089                types::ScanObservation::Unknown,
1090            )
1091            .await;
1092
1093        assert_matches!(saved_networks.lookup(&net_id).await.as_slice(), [config] => {
1094            assert!(config.has_ever_connected);
1095        });
1096        // If the specified network identifier is found, record_conenct_result should not mark
1097        // another config even if it could also have been used for the connect attempt.
1098        assert_matches!(saved_networks.lookup(&net_id_also_valid).await.as_slice(), [config] => {
1099            assert!(!config.has_ever_connected);
1100        });
1101    }
1102
1103    #[fuchsia::test]
1104    async fn test_record_connect_failure() {
1105        let saved_networks = SavedNetworksManager::new_for_test().await;
1106        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1107        let credential = Credential::None;
1108        let bssid = types::Bssid::from([1; 6]);
1109        let before_recording = fasync::MonotonicInstant::now();
1110
1111        // Verify that recording connect result does not save the network.
1112        saved_networks
1113            .record_connect_result(
1114                network_id.clone(),
1115                &credential,
1116                bssid,
1117                fidl_sme::ConnectResult {
1118                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1119                    ..fake_successful_connect_result()
1120                },
1121                types::ScanObservation::Unknown,
1122            )
1123            .await;
1124        assert!(saved_networks.lookup(&network_id).await.is_empty());
1125        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1126        assert_eq!(0, saved_networks.known_network_count().await);
1127
1128        // Record that the connect failed.
1129        assert!(
1130            saved_networks
1131                .store(network_id.clone(), credential.clone())
1132                .await
1133                .expect("Failed save network")
1134                .is_none()
1135        );
1136        saved_networks
1137            .record_connect_result(
1138                network_id.clone(),
1139                &credential,
1140                bssid,
1141                fidl_sme::ConnectResult {
1142                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1143                    ..fake_successful_connect_result()
1144                },
1145                types::ScanObservation::Unknown,
1146            )
1147            .await;
1148        saved_networks
1149            .record_connect_result(
1150                network_id.clone(),
1151                &credential,
1152                bssid,
1153                fidl_sme::ConnectResult {
1154                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1155                    is_credential_rejected: true,
1156                    ..fake_successful_connect_result()
1157                },
1158                types::ScanObservation::Unknown,
1159            )
1160            .await;
1161
1162        // Check that the failures were recorded correctly.
1163        assert_eq!(1, saved_networks.known_network_count().await);
1164        let saved_config = saved_networks
1165            .lookup(&network_id)
1166            .await
1167            .pop()
1168            .expect("Failed to get saved network config");
1169        let connect_failures =
1170            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1171        assert_matches!(connect_failures, failures => {
1172            // There are 2 failures. One is a general failure and one rejected credentials failure.
1173            assert_eq!(failures.len(), 2);
1174            assert!(failures.iter().any(|failure| failure.reason == FailureReason::GeneralFailure));
1175            assert!(failures.iter().any(|failure| failure.reason == FailureReason::CredentialRejected));
1176            // Both failures have the correct BSSID
1177            for failure in failures.iter() {
1178                assert_eq!(failure.bssid, bssid);
1179                assert_eq!(failure.bssid, bssid);
1180            }
1181        });
1182    }
1183
1184    #[fuchsia::test]
1185    async fn test_record_connect_cancelled_ignored() {
1186        let saved_networks = SavedNetworksManager::new_for_test().await;
1187        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1188        let credential = Credential::None;
1189        let bssid = types::Bssid::from([0; 6]);
1190        let before_recording = fasync::MonotonicInstant::now();
1191
1192        // Verify that recording connect result does not save the network.
1193        saved_networks
1194            .record_connect_result(
1195                network_id.clone(),
1196                &credential,
1197                bssid,
1198                fidl_sme::ConnectResult {
1199                    code: fidl_ieee80211::StatusCode::Canceled,
1200                    ..fake_successful_connect_result()
1201                },
1202                types::ScanObservation::Unknown,
1203            )
1204            .await;
1205        assert!(saved_networks.lookup(&network_id).await.is_empty());
1206        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1207        assert_eq!(0, saved_networks.known_network_count().await);
1208
1209        // Record that the connect was canceled.
1210        assert!(
1211            saved_networks
1212                .store(network_id.clone(), credential.clone())
1213                .await
1214                .expect("Failed save network")
1215                .is_none()
1216        );
1217        saved_networks
1218            .record_connect_result(
1219                network_id.clone(),
1220                &credential,
1221                bssid,
1222                fidl_sme::ConnectResult {
1223                    code: fidl_ieee80211::StatusCode::Canceled,
1224                    ..fake_successful_connect_result()
1225                },
1226                types::ScanObservation::Unknown,
1227            )
1228            .await;
1229
1230        // Check that there are no failures recorded for this saved network.
1231        assert_eq!(1, saved_networks.known_network_count().await);
1232        let saved_config = saved_networks
1233            .lookup(&network_id)
1234            .await
1235            .pop()
1236            .expect("Failed to get saved network config");
1237        let connect_failures =
1238            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1239        assert_eq!(0, connect_failures.len());
1240    }
1241
1242    #[fuchsia::test]
1243    async fn test_record_disconnect() {
1244        let saved_networks = SavedNetworksManager::new_for_test().await;
1245        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1246        let credential = Credential::Psk(vec![1; 32]);
1247        let data = random_connection_data();
1248
1249        saved_networks.record_disconnect(&id, &credential, data).await;
1250        // Verify that nothing happens if the network was not already saved.
1251        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1252        assert_eq!(saved_networks.known_network_count().await, 0);
1253
1254        // Save the network and record a disconnect.
1255        assert!(
1256            saved_networks
1257                .store(id.clone(), credential.clone())
1258                .await
1259                .expect("Failed to save network")
1260                .is_none()
1261        );
1262        saved_networks.record_disconnect(&id, &credential, data).await;
1263
1264        // Check that a data was recorded about the connection that just ended.
1265        let recent_connections = saved_networks
1266            .lookup(&id)
1267            .await
1268            .pop()
1269            .expect("Failed to get saved network")
1270            .perf_stats
1271            .past_connections
1272            .get_recent_for_network(fasync::MonotonicInstant::INFINITE_PAST);
1273        assert_matches!(recent_connections.as_slice(), [connection_data] => {
1274            assert_eq!(connection_data, &data);
1275        })
1276    }
1277
1278    #[fuchsia::test]
1279    async fn test_record_undirected_scan() {
1280        let saved_networks = SavedNetworksManager::new_for_test().await;
1281        let saved_seen_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1282        let saved_seen_network = types::NetworkIdentifierDetailed {
1283            ssid: saved_seen_id.ssid.clone(),
1284            security_type: types::SecurityTypeDetailed::Open,
1285        };
1286        let unsaved_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
1287        let unsaved_network = types::NetworkIdentifierDetailed {
1288            ssid: unsaved_id.ssid.clone(),
1289            security_type: types::SecurityTypeDetailed::Wpa2Personal,
1290        };
1291        let saved_unseen_id = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1292        let seen_credential = Credential::None;
1293        let unseen_credential = Credential::Password(b"password".to_vec());
1294
1295        // Save the networks
1296        assert!(
1297            saved_networks
1298                .store(saved_seen_id.clone(), seen_credential.clone())
1299                .await
1300                .expect("Failed to save network")
1301                .is_none()
1302        );
1303        assert!(
1304            saved_networks
1305                .store(saved_unseen_id.clone(), unseen_credential.clone())
1306                .await
1307                .expect("Failed to save network")
1308                .is_none()
1309        );
1310
1311        // Record passive scan results, including the saved network and another network.
1312        let results: HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>> = HashMap::from([
1313            (
1314                saved_seen_network,
1315                vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1316            ),
1317            (unsaved_network, vec![generate_random_bss()]),
1318        ]);
1319
1320        saved_networks
1321            .record_scan_result(vec!["some_other_ssid".try_into().unwrap()], &results)
1322            .await;
1323
1324        assert_matches!(saved_networks.lookup(&saved_seen_id).await.as_slice(), [config] => {
1325            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1326        });
1327        assert_matches!(saved_networks.lookup(&saved_unseen_id).await.as_slice(), [config] => {
1328            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1329        });
1330    }
1331
1332    #[fuchsia::test]
1333    async fn test_record_undirected_scan_with_upgraded_security() {
1334        // Test that if we see a different compatible (higher) scan result for a saved network that
1335        // could be used to connect, recording the scan results will change the hidden probability.
1336        let saved_networks = SavedNetworksManager::new_for_test().await;
1337        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1338        let credential = Credential::Password(b"credential".to_vec());
1339
1340        // Save the networks
1341        assert!(
1342            saved_networks
1343                .store(id.clone(), credential.clone())
1344                .await
1345                .expect("Failed to save network")
1346                .is_none()
1347        );
1348
1349        // Record passive scan results
1350        let results = HashMap::from([(
1351            types::NetworkIdentifierDetailed {
1352                ssid: id.ssid.clone(),
1353                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1354            },
1355            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1356        )]);
1357        saved_networks.record_scan_result(vec![], &results).await;
1358        // The network was seen in a passive scan, so hidden probability should be updated.
1359        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1360            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1361        });
1362    }
1363
1364    #[fuchsia::test]
1365    async fn test_record_undirected_scan_incompatible_credential() {
1366        // Test that if we see a different compatible (higher) scan result for a saved network that
1367        // could be used to connect, recording the scan results will change the hidden probability.
1368        let saved_networks = SavedNetworksManager::new_for_test().await;
1369        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1370        let credential = Credential::Psk(vec![8; 32]);
1371
1372        // Save the networks
1373        assert!(
1374            saved_networks
1375                .store(id.clone(), credential.clone())
1376                .await
1377                .expect("Failed to save network")
1378                .is_none()
1379        );
1380
1381        // Record passive scan results, including the saved network and another network.
1382        let results = HashMap::from([(
1383            types::NetworkIdentifierDetailed {
1384                ssid: id.ssid.clone(),
1385                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1386            },
1387            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1388        )]);
1389        saved_networks.record_scan_result(vec![], &results).await;
1390        // The network in the passive scan results was not compatible, so hidden probability should
1391        // not have been updated.
1392        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1393            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1394        });
1395    }
1396
1397    #[fuchsia::test]
1398    async fn test_record_directed_scan_for_upgraded_security() {
1399        // Test that if we see a different compatible (higher) scan result for a saved network that
1400        // could be used to connect in a directed scan, the hidden probability will not be lowered.
1401        let saved_networks = SavedNetworksManager::new_for_test().await;
1402        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa).unwrap();
1403        let credential = Credential::Password(b"credential".to_vec());
1404
1405        // Save the networks
1406        assert!(
1407            saved_networks
1408                .store(id.clone(), credential.clone())
1409                .await
1410                .expect("Failed to save network")
1411                .is_none()
1412        );
1413        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1414        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1415
1416        // Record directed scan results. The config's probability hidden should not be lowered
1417        // since we did not fail to see it in a directed scan.
1418        let results = HashMap::from([(
1419            types::NetworkIdentifierDetailed {
1420                ssid: id.ssid.clone(),
1421                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1422            },
1423            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1424        )]);
1425        let target = vec![id.ssid.clone()];
1426        saved_networks.record_scan_result(target, &results).await;
1427
1428        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1429        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1430    }
1431
1432    #[fuchsia::test]
1433    async fn test_record_directed_scan_for_incompatible_credential() {
1434        // Test that if we see a network that is not compatible because of the saved credential
1435        // (but is otherwise compatible), the directed scan is not considered successful and the
1436        // hidden probability of the config is lowered.
1437        let saved_networks = SavedNetworksManager::new_for_test().await;
1438        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1439        let credential = Credential::Psk(vec![11; 32]);
1440
1441        // Save the networks
1442        assert!(
1443            saved_networks
1444                .store(id.clone(), credential.clone())
1445                .await
1446                .expect("Failed to save network")
1447                .is_none()
1448        );
1449        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1450        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1451
1452        // Record directed scan results. The seen network does not match the saved network even
1453        // though security is compatible, since the security type is not compatible with the PSK.
1454        let target = vec![id.ssid.clone()];
1455        let results = HashMap::from([(
1456            types::NetworkIdentifierDetailed {
1457                ssid: id.ssid.clone(),
1458                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1459            },
1460            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1461        )]);
1462        saved_networks.record_scan_result(target, &results).await;
1463        // The hidden probability should have been lowered because a directed scan failed to find
1464        // the network.
1465        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1466        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1467    }
1468
1469    #[fuchsia::test]
1470    async fn test_record_directed_scan_no_ssid_match() {
1471        // Test that recording directed active scan results does not mistakenly match a config with
1472        // a network with a different SSID.
1473
1474        let saved_networks = SavedNetworksManager::new_for_test().await;
1475        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1476        let credential = Credential::Psk(vec![11; 32]);
1477        let diff_ssid = types::Ssid::try_from("other-ssid").unwrap();
1478
1479        // Save the networks
1480        assert!(
1481            saved_networks
1482                .store(id.clone(), credential.clone())
1483                .await
1484                .expect("Failed to save network")
1485                .is_none()
1486        );
1487        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1488        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1489
1490        // Record directed scan results. We target the saved network but see a different one.
1491        let target = vec![id.ssid.clone()];
1492        let results = HashMap::from([(
1493            types::NetworkIdentifierDetailed {
1494                ssid: diff_ssid,
1495                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1496            },
1497            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1498        )]);
1499        saved_networks.record_scan_result(target, &results).await;
1500
1501        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1502        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1503    }
1504
1505    #[fuchsia::test]
1506    async fn test_record_directed_one_not_compatible_one_compatible() {
1507        // Test that if we see two networks with the same SSID but only one is compatible, the scan
1508        // is recorded as successful for the config. In other words it isn't mistakenly recorded as
1509        // a failure because of the config that isn't compatible.
1510        let saved_networks = SavedNetworksManager::new_for_test().await;
1511        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1512        let credential = Credential::Password(b"foo-pass".to_vec());
1513
1514        // Save the networks
1515        assert!(
1516            saved_networks
1517                .store(id.clone(), credential.clone())
1518                .await
1519                .expect("Failed to save network")
1520                .is_none()
1521        );
1522        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1523        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1524
1525        // Record directed scan results. We see one network with the same SSID that doesn't match,
1526        // and one that does match.
1527        let target = vec![id.ssid.clone()];
1528        let results = HashMap::from([
1529            (
1530                types::NetworkIdentifierDetailed {
1531                    ssid: id.ssid.clone(),
1532                    security_type: types::SecurityTypeDetailed::Wpa1,
1533                },
1534                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1535            ),
1536            (
1537                types::NetworkIdentifierDetailed {
1538                    ssid: id.ssid.clone(),
1539                    security_type: types::SecurityTypeDetailed::Wpa2Personal,
1540                },
1541                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1542            ),
1543        ]);
1544        saved_networks.record_scan_result(target, &results).await;
1545        // Since the directed scan found a matching network, the hidden probability should not
1546        // have been lowered.
1547        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1548        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1549    }
1550
1551    #[fuchsia::test]
1552    async fn test_record_both_directed_and_undirected() {
1553        let saved_networks = SavedNetworksManager::new_for_test().await;
1554        let saved_undirected_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1555        let saved_undirected_network = types::NetworkIdentifierDetailed {
1556            ssid: saved_undirected_id.ssid.clone(),
1557            security_type: types::SecurityTypeDetailed::Open,
1558        };
1559        let saved_directed_id = NetworkIdentifier::try_from("bar", SecurityType::None).unwrap();
1560        let credential = Credential::None;
1561
1562        // Save the networks
1563        assert!(
1564            saved_networks
1565                .store(saved_undirected_id.clone(), credential.clone())
1566                .await
1567                .expect("Failed to save network")
1568                .is_none()
1569        );
1570        assert!(
1571            saved_networks
1572                .store(saved_directed_id.clone(), credential.clone())
1573                .await
1574                .expect("Failed to save network")
1575                .is_none()
1576        );
1577
1578        // Verify assumption
1579        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1580            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1581        });
1582
1583        // Record scan results
1584        let results = HashMap::from([(
1585            saved_undirected_network,
1586            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1587        )]);
1588        saved_networks.record_scan_result(vec![saved_directed_id.ssid.clone()], &results).await;
1589
1590        // The undirected (but seen) network is modified
1591        assert_matches!(saved_networks.lookup(&saved_undirected_id).await.as_slice(), [config] => {
1592            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1593        });
1594        // The directed (but *not* seen) network is modified
1595        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1596            assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1597        });
1598    }
1599
1600    #[fuchsia::test]
1601    fn evict_if_needed_removes_unconnected() {
1602        // this test is less meaningful when MAX_CONFIGS_PER_SSID is greater than 1, otherwise
1603        // the only saved configs should be removed when the max capacity is met, regardless of
1604        // whether it has been connected to.
1605        let unconnected_config = network_config("foo", "password");
1606        let mut connected_config = unconnected_config.clone();
1607        connected_config.has_ever_connected = false;
1608        let mut network_configs = vec![connected_config; MAX_CONFIGS_PER_SSID - 1];
1609        network_configs.insert(MAX_CONFIGS_PER_SSID / 2, unconnected_config.clone());
1610
1611        assert_eq!(evict_if_needed(&mut network_configs), Some(unconnected_config));
1612        assert_eq!(MAX_CONFIGS_PER_SSID - 1, network_configs.len());
1613        // check that everything left has been connected to before, only one removed is
1614        // the one that has never been connected to
1615        for config in network_configs.iter() {
1616            assert!(config.has_ever_connected);
1617        }
1618    }
1619
1620    #[fuchsia::test]
1621    fn evict_if_needed_already_has_space() {
1622        let mut configs = vec![];
1623        assert_eq!(evict_if_needed(&mut configs), None);
1624        let expected_cfgs: Vec<NetworkConfig> = vec![];
1625        assert_eq!(expected_cfgs, configs);
1626
1627        if MAX_CONFIGS_PER_SSID > 1 {
1628            let mut configs = vec![network_config("foo", "password")];
1629            assert_eq!(evict_if_needed(&mut configs), None);
1630            // if MAX_CONFIGS_PER_SSID is 1, this wouldn't be true
1631            assert_eq!(vec![network_config("foo", "password")], configs);
1632        }
1633    }
1634
1635    #[fuchsia::test]
1636    async fn clear() {
1637        let store_id = "clear";
1638        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1639        let saved_networks = create_saved_networks(store_id).await;
1640
1641        assert!(
1642            saved_networks
1643                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
1644                .await
1645                .expect("storing 'foo' failed")
1646                .is_none()
1647        );
1648        assert_eq!(
1649            vec![network_config("foo", "qwertyuio")],
1650            saved_networks.lookup(&network_id).await
1651        );
1652        assert_eq!(1, saved_networks.known_network_count().await);
1653
1654        saved_networks.clear().await.expect("failed to clear saved networks");
1655        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1656        assert_eq!(0, saved_networks.known_network_count().await);
1657
1658        // Load store from storage to verify it is also gone from persistent storage
1659        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1660        let store = PolicyStorage::new_with_id(store_id).await;
1661        let saved_networks =
1662            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1663                .await;
1664
1665        assert_eq!(0, saved_networks.known_network_count().await);
1666    }
1667
1668    impl std::fmt::Debug for SavedNetworksManager {
1669        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1670            f.debug_struct("SavedNetworksManager")
1671                .field("saved_networks", &self.saved_networks)
1672                .finish()
1673        }
1674    }
1675
1676    #[fuchsia::test]
1677    fn test_store_errors_cause_write_errors() {
1678        use fidl::endpoints::create_request_stream;
1679        use fidl_fuchsia_stash as fidl_stash;
1680        use futures::StreamExt;
1681        use std::sync::Arc;
1682        use std::sync::atomic::{AtomicBool, Ordering};
1683
1684        // Use a path for the persistent store that will cause write errors.
1685        let store_path_str = "/////";
1686        let mut exec = fasync::TestExecutor::new();
1687
1688        // Initialize stash proxies such that SavedNetworksManager initialize doesn't wait on
1689        // and doesn't load anything from the legacy stash.
1690        let (stash_client, mut request_stream) =
1691            create_request_stream::<fidl_stash::SecureStoreMarker>();
1692
1693        let read_from_stash = Arc::new(AtomicBool::new(false));
1694
1695        let _task = {
1696            let read_from_stash = read_from_stash.clone();
1697            fasync::Task::local(async move {
1698                while let Some(request) = request_stream.next().await {
1699                    match request.unwrap() {
1700                        fidl_stash::SecureStoreRequest::Identify { .. } => {}
1701                        fidl_stash::SecureStoreRequest::CreateAccessor {
1702                            accessor_request, ..
1703                        } => {
1704                            let read_from_stash = read_from_stash.clone();
1705                            fuchsia_async::EHandle::local().spawn_detached(async move {
1706                                let mut request_stream = accessor_request.into_stream();
1707                                while let Some(request) = request_stream.next().await {
1708                                    match request.unwrap() {
1709                                        fidl_stash::StoreAccessorRequest::ListPrefix { .. } => {
1710                                            read_from_stash.store(true, Ordering::Relaxed);
1711                                            // If we just drop the iterator, it should trigger a
1712                                            // read error.
1713                                        }
1714                                        _ => unreachable!(),
1715                                    }
1716                                }
1717                            });
1718                        }
1719                    }
1720                }
1721            })
1722        };
1723
1724        // Use a persistent store with the invalid file name and legacy stash which returns errors.
1725        let store =
1726            PolicyStorage::new_with_stash_proxy_and_id(stash_client.into_proxy(), store_path_str);
1727
1728        // Initialize the saved networks manager with the file that should cause write errors, the
1729        // legacy stash that will load nothing, and empty legacy known ess store file.
1730        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1731        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1732        let init_fut = SavedNetworksManager::new_with_storage(store, telemetry_sender);
1733        let mut init_fut = pin!(init_fut);
1734        let saved_networks = assert_matches!(exec.run_until_stalled(&mut init_fut), Poll::Ready(snm) => {
1735            snm
1736        });
1737
1738        // Save and remove networks and check that we get storage write errors
1739        let ssid = "foo";
1740        let credential = Credential::None;
1741        let network_id = NetworkIdentifier::try_from(ssid, SecurityType::None).unwrap();
1742        let save_fut = saved_networks.store(network_id.clone(), credential);
1743        let mut save_fut = pin!(save_fut);
1744
1745        assert_matches!(
1746            exec.run_until_stalled(&mut save_fut),
1747            Poll::Ready(Err(NetworkConfigError::FileWriteError))
1748        );
1749
1750        // The network should have been saved temporarily even if saving the network gives an error.
1751        assert_matches!(exec.run_until_stalled(&mut saved_networks.lookup(&network_id)), Poll::Ready(configs) => {
1752            assert_eq!(configs, vec![network_config(ssid, "")]);
1753        });
1754        assert_matches!(exec.run_until_stalled(&mut saved_networks.known_network_count()), Poll::Ready(count) => {
1755            assert_eq!(count, 1);
1756        });
1757    }
1758
1759    /// Create a saved networks manager and clear the contents. Storage ID should be different for
1760    /// each test so that they don't interfere.
1761    async fn create_saved_networks(store_id: &str) -> SavedNetworksManager {
1762        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1763        let store = PolicyStorage::new_with_id(store_id).await;
1764        let saved_networks =
1765            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1766                .await;
1767        saved_networks.clear().await.expect("failed to clear saved networks");
1768        saved_networks
1769    }
1770
1771    /// Convience function for creating network configs with default values as they would be
1772    /// initialized when read from KnownEssStore. Credential is password or none, and security
1773    /// type is WPA2 or none.
1774    fn network_config(ssid: &str, password: impl Into<Vec<u8>>) -> NetworkConfig {
1775        let credential = Credential::from_bytes(password.into());
1776        let id = NetworkIdentifier::try_from(ssid, credential.derived_security_type()).unwrap();
1777        let has_ever_connected = false;
1778        NetworkConfig::new(id, credential, has_ever_connected).unwrap()
1779    }
1780
1781    #[fuchsia::test]
1782    async fn record_metrics_when_called_on_class() {
1783        let store_id = generate_string();
1784        let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1785        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1786        let store = PolicyStorage::new_with_id(&store_id).await;
1787
1788        let saved_networks = SavedNetworksManager::new_with_storage(store, telemetry_sender).await;
1789        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1790        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1791
1792        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
1793        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1794        assert_eq!(0, saved_networks.known_network_count().await);
1795
1796        // Store a network and verify it was stored.
1797        assert!(
1798            saved_networks
1799                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
1800                .await
1801                .expect("storing 'foo' failed")
1802                .is_none()
1803        );
1804        assert_eq!(1, saved_networks.known_network_count().await);
1805
1806        // Store another network and verify.
1807        assert!(
1808            saved_networks
1809                .store(network_id_baz.clone(), Credential::Psk(vec![1; 32]))
1810                .await
1811                .expect("storing 'baz' with PSK failed")
1812                .is_none()
1813        );
1814        assert_eq!(2, saved_networks.known_network_count().await);
1815
1816        // Record metrics
1817        saved_networks.record_periodic_metrics().await;
1818
1819        // Verify metric is logged with two saved networks, which each have one config
1820        assert_matches!(telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::SavedNetworkCount { saved_network_count, config_count_per_saved_network })) => {
1821            assert_eq!(saved_network_count, 2);
1822            assert_eq!(config_count_per_saved_network, [1, 1]);
1823        });
1824    }
1825
1826    #[fuchsia::test]
1827    async fn probabilistic_choosing_of_hidden_networks() {
1828        // Create three networks with 1, 0, 0.5 hidden probability
1829        let id_hidden = types::NetworkIdentifier {
1830            ssid: types::Ssid::try_from("hidden").unwrap(),
1831            security_type: types::SecurityType::Wpa2,
1832        };
1833        let mut net_config_hidden = NetworkConfig::new(
1834            id_hidden.clone(),
1835            Credential::Password(b"password".to_vec()),
1836            false,
1837        )
1838        .expect("failed to create network config");
1839        net_config_hidden.hidden_probability = 1.0;
1840
1841        let id_not_hidden = types::NetworkIdentifier {
1842            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1843            security_type: types::SecurityType::Wpa2,
1844        };
1845        let mut net_config_not_hidden = NetworkConfig::new(
1846            id_not_hidden.clone(),
1847            Credential::Password(b"password".to_vec()),
1848            false,
1849        )
1850        .expect("failed to create network config");
1851        net_config_not_hidden.hidden_probability = 0.0;
1852
1853        let id_maybe_hidden = types::NetworkIdentifier {
1854            ssid: types::Ssid::try_from("maybe_hidden").unwrap(),
1855            security_type: types::SecurityType::Wpa2,
1856        };
1857        let mut net_config_maybe_hidden = NetworkConfig::new(
1858            id_maybe_hidden.clone(),
1859            Credential::Password(b"password".to_vec()),
1860            false,
1861        )
1862        .expect("failed to create network config");
1863        net_config_maybe_hidden.hidden_probability = 0.5;
1864
1865        let mut maybe_hidden_selection_count = 0;
1866        let mut hidden_selection_count = 0;
1867
1868        // Run selection many times, to ensure the probability is working as expected.
1869        for _ in 1..100 {
1870            let selected_networks = select_subset_potentially_hidden_networks(vec![
1871                net_config_hidden.clone(),
1872                net_config_not_hidden.clone(),
1873                net_config_maybe_hidden.clone(),
1874            ]);
1875            // The 1.0 probability should always be picked
1876            assert!(selected_networks.contains(&id_hidden));
1877            // The 0 probability should never be picked
1878            assert!(!selected_networks.contains(&id_not_hidden));
1879
1880            // Keep track of how often the networks were selected
1881            if selected_networks.contains(&id_maybe_hidden) {
1882                maybe_hidden_selection_count += 1;
1883            }
1884            if selected_networks.contains(&id_hidden) {
1885                hidden_selection_count += 1;
1886            }
1887        }
1888
1889        // The 0.5 probability network should be picked at least once, but not every time. With 100
1890        // runs, the chances of either of these assertions flaking is 1 / (0.5^100), i.e. 1 in 1e30.
1891        // Even with a hypothetical 1,000,000 test runs per day, there would be an average of 1e24
1892        // days between flakes due to this test.
1893        assert!(maybe_hidden_selection_count > 0);
1894        assert!(maybe_hidden_selection_count < hidden_selection_count);
1895    }
1896
1897    #[fuchsia::test]
1898    async fn test_select_high_probability_hidden_networks() {
1899        // Create three networks with 1, 0, 0.5 hidden probability
1900        let id_hidden = types::NetworkIdentifier {
1901            ssid: types::Ssid::try_from("hidden").unwrap(),
1902            security_type: types::SecurityType::Wpa2,
1903        };
1904        let mut net_config_hidden = NetworkConfig::new(
1905            id_hidden.clone(),
1906            Credential::Password(b"password".to_vec()),
1907            false,
1908        )
1909        .expect("failed to create network config");
1910        net_config_hidden.hidden_probability = 1.0;
1911
1912        let id_maybe_hidden_high = types::NetworkIdentifier {
1913            ssid: types::Ssid::try_from("maybe_hidden_high").unwrap(),
1914            security_type: types::SecurityType::Wpa2,
1915        };
1916        let mut net_config_maybe_hidden_high = NetworkConfig::new(
1917            id_maybe_hidden_high.clone(),
1918            Credential::Password(b"password".to_vec()),
1919            false,
1920        )
1921        .expect("failed to create network config");
1922        net_config_maybe_hidden_high.hidden_probability = 0.8;
1923
1924        let id_maybe_hidden_low = types::NetworkIdentifier {
1925            ssid: types::Ssid::try_from("maybe_hidden_low").unwrap(),
1926            security_type: types::SecurityType::Wpa2,
1927        };
1928        let mut net_config_maybe_hidden_low = NetworkConfig::new(
1929            id_maybe_hidden_low.clone(),
1930            Credential::Password(b"password".to_vec()),
1931            false,
1932        )
1933        .expect("failed to create network config");
1934        net_config_maybe_hidden_low.hidden_probability = 0.7;
1935
1936        let id_not_hidden = types::NetworkIdentifier {
1937            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1938            security_type: types::SecurityType::Wpa2,
1939        };
1940        let mut net_config_not_hidden = NetworkConfig::new(
1941            id_not_hidden.clone(),
1942            Credential::Password(b"password".to_vec()),
1943            false,
1944        )
1945        .expect("failed to create network config");
1946        net_config_not_hidden.hidden_probability = 0.0;
1947
1948        let selected_networks = select_high_probability_hidden_networks(vec![
1949            net_config_hidden.clone(),
1950            net_config_maybe_hidden_high.clone(),
1951            net_config_maybe_hidden_low.clone(),
1952            net_config_not_hidden.clone(),
1953        ]);
1954
1955        // The 1.0 probability should always be picked
1956        assert!(selected_networks.contains(&id_hidden));
1957        // The high probability should always be picked
1958        assert!(selected_networks.contains(&id_maybe_hidden_high));
1959        // The low probability should never be picked
1960        assert!(!selected_networks.contains(&id_maybe_hidden_low));
1961        // The 0 probability should never be picked
1962        assert!(!selected_networks.contains(&id_not_hidden));
1963    }
1964
1965    #[fuchsia::test]
1966    async fn test_record_not_seen_active_scan() {
1967        // Test that if we update that we haven't seen a couple of networks in active scans, their
1968        // hidden probability is updated.
1969        let saved_networks = SavedNetworksManager::new_for_test().await;
1970
1971        // Seen in active scans
1972        let id_1 = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
1973        let credential_1 = Credential::Password(b"some_password".to_vec());
1974        let id_2 = NetworkIdentifier::try_from("bar", SecurityType::Wpa3).unwrap();
1975        let credential_2 = Credential::Password(b"another_password".to_vec());
1976        // Seen in active scan but not saved
1977        let id_3 = NetworkIdentifier::try_from("baz", SecurityType::None).unwrap();
1978        // Saved and targeted in active scan but not seen
1979        let id_4 = NetworkIdentifier::try_from("foobar", SecurityType::None).unwrap();
1980        let credential_4 = Credential::None;
1981
1982        // Save 3 of the 4 networks
1983        assert!(
1984            saved_networks
1985                .store(id_1.clone(), credential_1)
1986                .await
1987                .expect("failed to store network")
1988                .is_none()
1989        );
1990        assert!(
1991            saved_networks
1992                .store(id_2.clone(), credential_2)
1993                .await
1994                .expect("failed to store network")
1995                .is_none()
1996        );
1997        assert!(
1998            saved_networks
1999                .store(id_4.clone(), credential_4)
2000                .await
2001                .expect("failed to store network")
2002                .is_none()
2003        );
2004        // Check that the saved networks have the default hidden probability so later we can just
2005        // check that the probability has changed.
2006        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2007        assert_eq!(config_1.hidden_probability, PROB_HIDDEN_DEFAULT);
2008        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2009        assert_eq!(config_2.hidden_probability, PROB_HIDDEN_DEFAULT);
2010        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2011        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2012
2013        let not_seen_ids = vec![id_1.ssid.clone(), id_2.ssid.clone(), id_3.ssid.clone()];
2014        saved_networks.record_scan_result(not_seen_ids, &HashMap::new()).await;
2015
2016        // Check that the configs' hidden probability has decreased
2017        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2018        assert!(config_1.hidden_probability < PROB_HIDDEN_DEFAULT);
2019        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2020        assert!(config_2.hidden_probability < PROB_HIDDEN_DEFAULT);
2021
2022        // Check that for the network that was target but not seen in the active scan, its hidden
2023        // probability isn't lowered.
2024        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2025        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2026
2027        // Check that a config was not saved for the identifier that was not saved before.
2028        assert!(saved_networks.lookup(&id_3).await.is_empty());
2029    }
2030
2031    #[fuchsia::test]
2032    async fn test_update_scan_stats_for_single_bss() {
2033        // Record multiple scans for a network where there is only 1 BSS for the network. The
2034        // config should be considered likely single BSS.
2035        let saved_networks = SavedNetworksManager::new_for_test().await;
2036
2037        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2038        let credential = Credential::Password(b"some_password".to_vec());
2039        assert!(
2040            saved_networks
2041                .store(id.clone(), credential.clone())
2042                .await
2043                .expect("failed to store network")
2044                .is_none()
2045        );
2046
2047        let id_detailed = types::NetworkIdentifierDetailed {
2048            ssid: id.ssid.clone(),
2049            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2050        };
2051        let scan_results = HashMap::from([(
2052            id_detailed.clone(),
2053            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2054        )]);
2055
2056        // likely has one BSS
2057        for _ in 0..5 {
2058            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2059        }
2060
2061        let is_single_bss = saved_networks
2062            .is_network_single_bss(&id, &credential)
2063            .await
2064            .expect("failed to lookup if network is single BSS");
2065        assert!(is_single_bss);
2066    }
2067
2068    #[fuchsia::test]
2069    async fn test_update_scan_stats_for_multiple_bss_at_least_once() {
2070        // Record multiple scans for a network where there are multiple BSS. The network config
2071        // should say that the network is not single BSS.
2072        let saved_networks = SavedNetworksManager::new_for_test().await;
2073
2074        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2075        let credential = Credential::Password(b"some_password".to_vec());
2076        assert!(
2077            saved_networks
2078                .store(id.clone(), credential.clone())
2079                .await
2080                .expect("failed to store network")
2081                .is_none()
2082        );
2083
2084        let id_detailed = types::NetworkIdentifierDetailed {
2085            ssid: id.ssid.clone(),
2086            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2087        };
2088        let scan_results_single = HashMap::from([(
2089            id_detailed.clone(),
2090            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2091        )]);
2092
2093        let scan_results_multi = HashMap::from([(
2094            id_detailed.clone(),
2095            vec![
2096                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2097                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2098            ],
2099        )]);
2100
2101        // Record some scan results with one BSS, and record once with multiple BSS.
2102        for _ in 0..2 {
2103            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2104        }
2105
2106        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_multi).await;
2107        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2108
2109        // The one scan with multiple BSS results should make the network determined to be
2110        // multi BSS.
2111        let is_single_bss = saved_networks
2112            .is_network_single_bss(&id, &credential)
2113            .await
2114            .expect("failed to lookup if network is single BSS");
2115        assert!(!is_single_bss);
2116    }
2117
2118    #[fuchsia::test]
2119    async fn test_record_scan_more_than_once_to_decide_single_bss() {
2120        // Test that a network is not decided to be single BSS after only one scan.
2121        let saved_networks = SavedNetworksManager::new_for_test().await;
2122
2123        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2124        let credential = Credential::Password(b"some_password".to_vec());
2125        assert!(
2126            saved_networks
2127                .store(id.clone(), credential.clone())
2128                .await
2129                .expect("failed to store network")
2130                .is_none()
2131        );
2132
2133        let id_detailed = types::NetworkIdentifierDetailed {
2134            ssid: id.ssid.clone(),
2135            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2136        };
2137        let scan_results = HashMap::from([(
2138            id_detailed,
2139            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2140        )]);
2141
2142        // Record the scan multiple times, since multiple scans are needed to decide the network
2143        // likely has one BSS
2144        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2145
2146        let is_single_bss = saved_networks
2147            .is_network_single_bss(&id, &credential)
2148            .await
2149            .expect("failed to lookup if network is single BSS");
2150        assert!(!is_single_bss);
2151    }
2152
2153    #[fuchsia::test]
2154    async fn test_get_past_connections() {
2155        let saved_networks_manager = SavedNetworksManager::new_for_test().await;
2156
2157        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2158        let credential = Credential::Password(b"some_password".to_vec());
2159        let mut config = NetworkConfig::new(id.clone(), credential.clone(), true)
2160            .expect("failed to create config");
2161        let mut past_connections = HistoricalListsByBssid::new();
2162
2163        // Add two past connections with the same bssid
2164        let data_1 = random_connection_data();
2165        let bssid_1 = data_1.bssid;
2166        let mut data_2 = random_connection_data();
2167        data_2.bssid = bssid_1;
2168        past_connections.add(bssid_1, data_1);
2169        past_connections.add(bssid_1, data_2);
2170
2171        // Add a past connection with different bssid
2172        let data_3 = random_connection_data();
2173        let bssid_2 = data_3.bssid;
2174        past_connections.add(bssid_2, data_3);
2175        config.perf_stats.past_connections = past_connections;
2176
2177        // Create SavedNetworksManager with configs that have past connections
2178        assert!(
2179            saved_networks_manager
2180                .saved_networks
2181                .lock()
2182                .await
2183                .insert(id.clone(), vec![config])
2184                .is_none()
2185        );
2186
2187        // Check that get_past_connections gets the two PastConnectionLists for the BSSIDs.
2188        let mut expected_past_connections = PastConnectionList::default();
2189        expected_past_connections.add(data_1);
2190        expected_past_connections.add(data_2);
2191        let actual_past_connections =
2192            saved_networks_manager.get_past_connections(&id, &credential, &bssid_1).await;
2193        assert_eq!(actual_past_connections, expected_past_connections);
2194
2195        let mut expected_past_connections = PastConnectionList::default();
2196        expected_past_connections.add(data_3);
2197        let actual_past_connections =
2198            saved_networks_manager.get_past_connections(&id, &credential, &bssid_2).await;
2199        assert_eq!(actual_past_connections, expected_past_connections);
2200
2201        // Check that get_past_connections will not get the PastConnectionLists if the specified
2202        // Credential is different.
2203        let actual_past_connections = saved_networks_manager
2204            .get_past_connections(&id, &Credential::Password(b"other-password".to_vec()), &bssid_1)
2205            .await;
2206        assert_eq!(actual_past_connections, PastConnectionList::default());
2207    }
2208
2209    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
2210        fidl_sme::ConnectResult {
2211            code: fidl_ieee80211::StatusCode::Success,
2212            is_credential_rejected: false,
2213            is_reconnect: false,
2214        }
2215    }
2216}