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        // TODO(https://fxbug.dev/462514157): Map Owe and OpenOweTransition to correct security types
615        Owe => vec![SecurityType::None],
616        OpenOweTransition => vec![SecurityType::None],
617        Open => vec![SecurityType::None],
618        Unknown => vec![],
619    }
620}
621
622pub fn security_is_compatible(
623    scan_security: &types::SecurityTypeDetailed,
624    credential: &Credential,
625) -> bool {
626    if (scan_security == &types::SecurityTypeDetailed::Wpa3Personal
627        || scan_security == &types::SecurityTypeDetailed::Wpa3Enterprise)
628        && let Credential::Psk(_) = credential
629    {
630        return false;
631    }
632    true
633}
634
635/// If the list of configs is at capacity for the number of saved configs per SSID,
636/// remove a saved network that has never been successfully connected to. If all have
637/// been successfully connected to, remove any. If a network config is evicted, that connection
638/// is forgotten for future connections.
639/// TODO(https://fxbug.dev/42117293) - when network configs record information about successful connections,
640/// use this to make a better decision what to forget if all networks have connected before.
641/// TODO(https://fxbug.dev/42117730) - make sure that we disconnect from the network if we evict a network config
642/// for a network we are currently connected to.
643fn evict_if_needed(configs: &mut Vec<NetworkConfig>) -> Option<NetworkConfig> {
644    if configs.len() < MAX_CONFIGS_PER_SSID {
645        return None;
646    }
647
648    for i in 0..configs.len() {
649        if let Some(config) = configs.get(i)
650            && !config.has_ever_connected
651        {
652            return Some(configs.remove(i));
653        }
654    }
655    // If all saved networks have connected, remove the first network
656    Some(configs.remove(0))
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use crate::config_management::{
663        HistoricalListsByBssid, PROB_HIDDEN_DEFAULT, PROB_HIDDEN_IF_CONNECT_ACTIVE,
664        PROB_HIDDEN_IF_CONNECT_PASSIVE, PROB_HIDDEN_IF_SEEN_PASSIVE,
665    };
666    use crate::util::testing::{generate_random_bss, generate_string, random_connection_data};
667    use assert_matches::assert_matches;
668    use futures::channel::mpsc;
669    use futures::task::Poll;
670    use std::pin::pin;
671    use test_case::test_case;
672
673    #[fuchsia::test]
674    async fn store_and_lookup() {
675        let store_id = generate_string();
676        let saved_networks = create_saved_networks(&store_id).await;
677        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
678
679        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
680        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
681        assert_eq!(0, saved_networks.known_network_count().await);
682
683        // Store a network and verify it was stored.
684        assert!(
685            saved_networks
686                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
687                .await
688                .expect("storing 'foo' failed")
689                .is_none()
690        );
691        assert_eq!(
692            vec![network_config("foo", "qwertyuio")],
693            saved_networks.lookup(&network_id_foo).await
694        );
695        assert_eq!(1, saved_networks.known_network_count().await);
696
697        // Store another network with the same SSID.
698        let popped_network = saved_networks
699            .store(network_id_foo.clone(), Credential::Password(b"12345678".to_vec()))
700            .await
701            .expect("storing 'foo' a second time failed");
702        assert_eq!(popped_network, Some(network_config("foo", "qwertyuio")));
703
704        // There should only be one saved "foo" network because MAX_CONFIGS_PER_SSID is 1.
705        // When this constant becomes greater than 1, both network configs should be found
706        assert_eq!(
707            vec![network_config("foo", "12345678")],
708            saved_networks.lookup(&network_id_foo).await
709        );
710        assert_eq!(1, saved_networks.known_network_count().await);
711
712        // Store another network and verify.
713        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
714        let psk = Credential::Psk(vec![1; 32]);
715        let config_baz = NetworkConfig::new(network_id_baz.clone(), psk.clone(), false)
716            .expect("failed to create network config");
717        assert!(
718            saved_networks
719                .store(network_id_baz.clone(), psk)
720                .await
721                .expect("storing 'baz' with PSK failed")
722                .is_none()
723        );
724        assert_eq!(vec![config_baz.clone()], saved_networks.lookup(&network_id_baz).await);
725        assert_eq!(2, saved_networks.known_network_count().await);
726
727        // Saved networks should persist when we create a saved networks manager with the same ID.
728        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
729        let store = PolicyStorage::new_with_id(&store_id).await;
730
731        let saved_networks =
732            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
733                .await;
734        assert_eq!(
735            vec![network_config("foo", "12345678")],
736            saved_networks.lookup(&network_id_foo).await
737        );
738        assert_eq!(vec![config_baz], saved_networks.lookup(&network_id_baz).await);
739        assert_eq!(2, saved_networks.known_network_count().await);
740    }
741
742    #[fuchsia::test]
743    async fn store_twice() {
744        let saved_networks = SavedNetworksManager::new_for_test().await;
745        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
746
747        assert!(
748            saved_networks
749                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
750                .await
751                .expect("storing 'foo' failed")
752                .is_none()
753        );
754        let popped_network = saved_networks
755            .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
756            .await
757            .expect("storing 'foo' a second time failed");
758        // Because the same network was stored twice, nothing was evicted, so popped_network == None
759        assert_eq!(popped_network, None);
760        let expected_cfgs = vec![network_config("foo", "qwertyuio")];
761        assert_eq!(expected_cfgs, saved_networks.lookup(&network_id).await);
762        assert_eq!(1, saved_networks.known_network_count().await);
763    }
764
765    #[fuchsia::test]
766    async fn store_many_same_ssid() {
767        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
768        let saved_networks = SavedNetworksManager::new_for_test().await;
769
770        // save max + 1 networks with same SSID and different credentials
771        for i in 0..MAX_CONFIGS_PER_SSID + 1 {
772            let mut password = b"password".to_vec();
773            password.push(i as u8);
774            let popped_network = saved_networks
775                .store(network_id.clone(), Credential::Password(password))
776                .await
777                .expect("Failed to saved network");
778            if i >= MAX_CONFIGS_PER_SSID {
779                assert!(popped_network.is_some());
780            } else {
781                assert!(popped_network.is_none());
782            }
783        }
784
785        // since none have been connected to yet, we don't care which config was removed
786        assert_eq!(MAX_CONFIGS_PER_SSID, saved_networks.lookup(&network_id).await.len());
787    }
788
789    #[fuchsia::test]
790    async fn store_and_remove() {
791        let store_id = generate_string();
792        let saved_networks = create_saved_networks(&store_id).await;
793
794        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
795        let credential = Credential::Password(b"qwertyuio".to_vec());
796        assert!(saved_networks.lookup(&network_id).await.is_empty());
797        assert_eq!(0, saved_networks.known_network_count().await);
798
799        // Store a network and verify it was stored.
800        assert!(
801            saved_networks
802                .store(network_id.clone(), credential.clone())
803                .await
804                .expect("storing 'foo' failed")
805                .is_none()
806        );
807        assert_eq!(
808            vec![network_config("foo", "qwertyuio")],
809            saved_networks.lookup(&network_id).await
810        );
811        assert_eq!(1, saved_networks.known_network_count().await);
812
813        // Remove a network with the same NetworkIdentifier but differenct credential and verify
814        // that the saved network is unaffected.
815        assert!(
816            !saved_networks
817                .remove(network_id.clone(), Credential::Password(b"diff-password".to_vec()))
818                .await
819                .expect("removing 'foo' failed")
820        );
821        assert_eq!(1, saved_networks.known_network_count().await);
822
823        // Remove the network and check it is gone
824        assert!(
825            saved_networks
826                .remove(network_id.clone(), credential.clone())
827                .await
828                .expect("removing 'foo' failed")
829        );
830        assert_eq!(0, saved_networks.known_network_count().await);
831        // Check that the key in the saved networks manager's internal hashmap was removed.
832        assert!(saved_networks.saved_networks.lock().await.get(&network_id).is_none());
833
834        // If we try to remove the network again, we won't get an error and nothing happens
835        assert!(
836            !saved_networks
837                .remove(network_id.clone(), credential)
838                .await
839                .expect("removing 'foo' failed")
840        );
841
842        // Check that removal persists.
843        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
844        let store = PolicyStorage::new_with_id(&store_id).await;
845        let saved_networks =
846            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
847                .await;
848        assert_eq!(0, saved_networks.known_network_count().await);
849        assert!(saved_networks.lookup(&network_id).await.is_empty());
850    }
851
852    #[fuchsia::test]
853    fn sme_protection_converts_to_lower_compatible() {
854        use fidl_sme::Protection::*;
855        let lower_compatible_pairs = vec![
856            (Wpa3Enterprise, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
857            (Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
858            (Wpa2Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
859            (Wpa2Enterprise, vec![SecurityType::Wpa, SecurityType::Wpa2]),
860            (Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
861            (Wpa1Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
862            (Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
863            (Wpa1Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
864            (Wpa1, vec![SecurityType::Wpa]),
865            (Wep, vec![SecurityType::Wep]),
866            (Open, vec![SecurityType::None]),
867            (Unknown, vec![]),
868        ];
869        for (detailed_security, security) in lower_compatible_pairs {
870            assert_eq!(compatible_policy_securities(&detailed_security), security);
871        }
872    }
873
874    #[fuchsia::test]
875    async fn lookup_compatible_returns_both_compatible_configs() {
876        let saved_networks = SavedNetworksManager::new_for_test().await;
877        let ssid = types::Ssid::try_from("foo").unwrap();
878        let network_id_wpa2 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
879        let network_id_wpa3 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
880        let credential_wpa2 = Credential::Password(b"password".to_vec());
881        let credential_wpa3 = Credential::Password(b"wpa3-password".to_vec());
882
883        // Check that lookup_compatible does not modify the SavedNetworksManager and returns an
884        // empty vector if there is no matching config.
885        let results = saved_networks
886            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
887            .await;
888        assert!(results.is_empty());
889        assert_eq!(saved_networks.known_network_count().await, 0);
890
891        // Store a couple of network configs that could both be use to connect to a WPA2/WPA3
892        // network.
893        assert!(
894            saved_networks
895                .store(network_id_wpa2.clone(), credential_wpa2.clone())
896                .await
897                .expect("Failed to store network")
898                .is_none()
899        );
900        assert!(
901            saved_networks
902                .store(network_id_wpa3.clone(), credential_wpa3.clone())
903                .await
904                .expect("Failed to store network")
905                .is_none()
906        );
907        // Store a network with the same SSID but a not-compatible security type.
908        let network_id_wep = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa);
909        assert!(
910            saved_networks
911                .store(network_id_wep.clone(), Credential::Password(b"abcdefgh".to_vec()))
912                .await
913                .expect("Failed to store network")
914                .is_none()
915        );
916
917        let results = saved_networks
918            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
919            .await;
920        let expected_config_wpa2 = NetworkConfig::new(network_id_wpa2, credential_wpa2, false)
921            .expect("Failed to create config");
922        let expected_config_wpa3 = NetworkConfig::new(network_id_wpa3, credential_wpa3, false)
923            .expect("Failed to create config");
924        assert_eq!(results.len(), 2);
925        assert!(results.contains(&expected_config_wpa2));
926        assert!(results.contains(&expected_config_wpa3));
927    }
928
929    #[test_case(types::SecurityTypeDetailed::Wpa3Personal)]
930    #[test_case(types::SecurityTypeDetailed::Wpa3Enterprise)]
931    #[fuchsia::test(add_test_attr = false)]
932    fn lookup_compatible_does_not_return_wpa3_psk(
933        wpa3_detailed_security: types::SecurityTypeDetailed,
934    ) {
935        let mut exec = fasync::TestExecutor::new();
936        let saved_networks = exec.run_singlethreaded(SavedNetworksManager::new_for_test());
937
938        // Store a WPA3 config with a password that will match and a PSK config that won't match
939        // to a WPA3 network.
940        let ssid = types::Ssid::try_from("foo").unwrap();
941        let network_id_psk = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
942        let network_id_password = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
943        let credential_psk = Credential::Psk(vec![5; 32]);
944        let credential_password = Credential::Password(b"mypassword".to_vec());
945        assert!(
946            exec.run_singlethreaded(
947                saved_networks.store(network_id_psk.clone(), credential_psk.clone()),
948            )
949            .expect("Failed to store network")
950            .is_none()
951        );
952        assert!(
953            exec.run_singlethreaded(
954                saved_networks.store(network_id_password.clone(), credential_password.clone()),
955            )
956            .expect("Failed to store network")
957            .is_none()
958        );
959
960        // Only the WPA3 config with a credential should be returned.
961        let expected_config_wpa3 =
962            NetworkConfig::new(network_id_password, credential_password, false)
963                .expect("Failed to create configc");
964        let results = exec
965            .run_singlethreaded(saved_networks.lookup_compatible(&ssid, wpa3_detailed_security));
966        assert_eq!(results, vec![expected_config_wpa3]);
967    }
968
969    #[fuchsia::test]
970    async fn connect_network() {
971        let store_id = generate_string();
972
973        let saved_networks = create_saved_networks(&store_id).await;
974
975        let network_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
976        let credential = Credential::Password(b"password".to_vec());
977        let bssid = types::Bssid::from([4; 6]);
978
979        // If connect and network hasn't been saved, we should not save the network.
980        saved_networks
981            .record_connect_result(
982                network_id.clone(),
983                &credential,
984                bssid,
985                fake_successful_connect_result(),
986                types::ScanObservation::Unknown,
987            )
988            .await;
989        assert!(saved_networks.lookup(&network_id).await.is_empty());
990        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
991        assert_eq!(0, saved_networks.known_network_count().await);
992
993        // Save the network and record a successful connection.
994        assert!(
995            saved_networks
996                .store(network_id.clone(), credential.clone())
997                .await
998                .expect("Failed save network")
999                .is_none()
1000        );
1001
1002        let config = network_config("bar", "password");
1003        assert_eq!(vec![config], saved_networks.lookup(&network_id).await);
1004
1005        saved_networks
1006            .record_connect_result(
1007                network_id.clone(),
1008                &credential,
1009                bssid,
1010                fake_successful_connect_result(),
1011                types::ScanObservation::Unknown,
1012            )
1013            .await;
1014
1015        // The network should be saved with the connection recorded. We should not have recorded
1016        // that the network was connected to passively or actively.
1017        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1018            assert!(config.has_ever_connected);
1019            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1020        });
1021
1022        saved_networks
1023            .record_connect_result(
1024                network_id.clone(),
1025                &credential,
1026                bssid,
1027                fake_successful_connect_result(),
1028                types::ScanObservation::Active,
1029            )
1030            .await;
1031        // We should now see that we connected to the network after an active scan.
1032        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1033            assert!(config.has_ever_connected);
1034            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1035        });
1036
1037        saved_networks
1038            .record_connect_result(
1039                network_id.clone(),
1040                &credential,
1041                bssid,
1042                fake_successful_connect_result(),
1043                types::ScanObservation::Passive,
1044            )
1045            .await;
1046        // The config should have a lower hidden probability after connecting after a passive scan.
1047        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1048            assert!(config.has_ever_connected);
1049            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1050        });
1051
1052        // Success connects should be saved as persistent data.
1053        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1054        let store = PolicyStorage::new_with_id(&store_id).await;
1055        let saved_networks =
1056            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1057                .await;
1058        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1059            assert!(config.has_ever_connected);
1060        });
1061    }
1062
1063    #[fuchsia::test]
1064    async fn test_record_connect_updates_one() {
1065        let saved_networks = SavedNetworksManager::new_for_test().await;
1066        let net_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1067        let net_id_also_valid = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
1068        let credential = Credential::Password(b"some_password".to_vec());
1069        let bssid = types::Bssid::from([2; 6]);
1070
1071        // Save the networks and record a successful connection.
1072        assert!(
1073            saved_networks
1074                .store(net_id.clone(), credential.clone())
1075                .await
1076                .expect("Failed save network")
1077                .is_none()
1078        );
1079        assert!(
1080            saved_networks
1081                .store(net_id_also_valid.clone(), credential.clone())
1082                .await
1083                .expect("Failed save network")
1084                .is_none()
1085        );
1086        saved_networks
1087            .record_connect_result(
1088                net_id.clone(),
1089                &credential,
1090                bssid,
1091                fake_successful_connect_result(),
1092                types::ScanObservation::Unknown,
1093            )
1094            .await;
1095
1096        assert_matches!(saved_networks.lookup(&net_id).await.as_slice(), [config] => {
1097            assert!(config.has_ever_connected);
1098        });
1099        // If the specified network identifier is found, record_conenct_result should not mark
1100        // another config even if it could also have been used for the connect attempt.
1101        assert_matches!(saved_networks.lookup(&net_id_also_valid).await.as_slice(), [config] => {
1102            assert!(!config.has_ever_connected);
1103        });
1104    }
1105
1106    #[fuchsia::test]
1107    async fn test_record_connect_failure() {
1108        let saved_networks = SavedNetworksManager::new_for_test().await;
1109        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1110        let credential = Credential::None;
1111        let bssid = types::Bssid::from([1; 6]);
1112        let before_recording = fasync::MonotonicInstant::now();
1113
1114        // Verify that recording connect result does not save the network.
1115        saved_networks
1116            .record_connect_result(
1117                network_id.clone(),
1118                &credential,
1119                bssid,
1120                fidl_sme::ConnectResult {
1121                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1122                    ..fake_successful_connect_result()
1123                },
1124                types::ScanObservation::Unknown,
1125            )
1126            .await;
1127        assert!(saved_networks.lookup(&network_id).await.is_empty());
1128        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1129        assert_eq!(0, saved_networks.known_network_count().await);
1130
1131        // Record that the connect failed.
1132        assert!(
1133            saved_networks
1134                .store(network_id.clone(), credential.clone())
1135                .await
1136                .expect("Failed save network")
1137                .is_none()
1138        );
1139        saved_networks
1140            .record_connect_result(
1141                network_id.clone(),
1142                &credential,
1143                bssid,
1144                fidl_sme::ConnectResult {
1145                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1146                    ..fake_successful_connect_result()
1147                },
1148                types::ScanObservation::Unknown,
1149            )
1150            .await;
1151        saved_networks
1152            .record_connect_result(
1153                network_id.clone(),
1154                &credential,
1155                bssid,
1156                fidl_sme::ConnectResult {
1157                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1158                    is_credential_rejected: true,
1159                    ..fake_successful_connect_result()
1160                },
1161                types::ScanObservation::Unknown,
1162            )
1163            .await;
1164
1165        // Check that the failures were recorded correctly.
1166        assert_eq!(1, saved_networks.known_network_count().await);
1167        let saved_config = saved_networks
1168            .lookup(&network_id)
1169            .await
1170            .pop()
1171            .expect("Failed to get saved network config");
1172        let connect_failures =
1173            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1174        assert_matches!(connect_failures, failures => {
1175            // There are 2 failures. One is a general failure and one rejected credentials failure.
1176            assert_eq!(failures.len(), 2);
1177            assert!(failures.iter().any(|failure| failure.reason == FailureReason::GeneralFailure));
1178            assert!(failures.iter().any(|failure| failure.reason == FailureReason::CredentialRejected));
1179            // Both failures have the correct BSSID
1180            for failure in failures.iter() {
1181                assert_eq!(failure.bssid, bssid);
1182                assert_eq!(failure.bssid, bssid);
1183            }
1184        });
1185    }
1186
1187    #[fuchsia::test]
1188    async fn test_record_connect_cancelled_ignored() {
1189        let saved_networks = SavedNetworksManager::new_for_test().await;
1190        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1191        let credential = Credential::None;
1192        let bssid = types::Bssid::from([0; 6]);
1193        let before_recording = fasync::MonotonicInstant::now();
1194
1195        // Verify that recording connect result does not save the network.
1196        saved_networks
1197            .record_connect_result(
1198                network_id.clone(),
1199                &credential,
1200                bssid,
1201                fidl_sme::ConnectResult {
1202                    code: fidl_ieee80211::StatusCode::Canceled,
1203                    ..fake_successful_connect_result()
1204                },
1205                types::ScanObservation::Unknown,
1206            )
1207            .await;
1208        assert!(saved_networks.lookup(&network_id).await.is_empty());
1209        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1210        assert_eq!(0, saved_networks.known_network_count().await);
1211
1212        // Record that the connect was canceled.
1213        assert!(
1214            saved_networks
1215                .store(network_id.clone(), credential.clone())
1216                .await
1217                .expect("Failed save network")
1218                .is_none()
1219        );
1220        saved_networks
1221            .record_connect_result(
1222                network_id.clone(),
1223                &credential,
1224                bssid,
1225                fidl_sme::ConnectResult {
1226                    code: fidl_ieee80211::StatusCode::Canceled,
1227                    ..fake_successful_connect_result()
1228                },
1229                types::ScanObservation::Unknown,
1230            )
1231            .await;
1232
1233        // Check that there are no failures recorded for this saved network.
1234        assert_eq!(1, saved_networks.known_network_count().await);
1235        let saved_config = saved_networks
1236            .lookup(&network_id)
1237            .await
1238            .pop()
1239            .expect("Failed to get saved network config");
1240        let connect_failures =
1241            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1242        assert_eq!(0, connect_failures.len());
1243    }
1244
1245    #[fuchsia::test]
1246    async fn test_record_disconnect() {
1247        let saved_networks = SavedNetworksManager::new_for_test().await;
1248        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1249        let credential = Credential::Psk(vec![1; 32]);
1250        let data = random_connection_data();
1251
1252        saved_networks.record_disconnect(&id, &credential, data).await;
1253        // Verify that nothing happens if the network was not already saved.
1254        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1255        assert_eq!(saved_networks.known_network_count().await, 0);
1256
1257        // Save the network and record a disconnect.
1258        assert!(
1259            saved_networks
1260                .store(id.clone(), credential.clone())
1261                .await
1262                .expect("Failed to save network")
1263                .is_none()
1264        );
1265        saved_networks.record_disconnect(&id, &credential, data).await;
1266
1267        // Check that a data was recorded about the connection that just ended.
1268        let recent_connections = saved_networks
1269            .lookup(&id)
1270            .await
1271            .pop()
1272            .expect("Failed to get saved network")
1273            .perf_stats
1274            .past_connections
1275            .get_recent_for_network(fasync::MonotonicInstant::INFINITE_PAST);
1276        assert_matches!(recent_connections.as_slice(), [connection_data] => {
1277            assert_eq!(connection_data, &data);
1278        })
1279    }
1280
1281    #[fuchsia::test]
1282    async fn test_record_undirected_scan() {
1283        let saved_networks = SavedNetworksManager::new_for_test().await;
1284        let saved_seen_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1285        let saved_seen_network = types::NetworkIdentifierDetailed {
1286            ssid: saved_seen_id.ssid.clone(),
1287            security_type: types::SecurityTypeDetailed::Open,
1288        };
1289        let unsaved_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
1290        let unsaved_network = types::NetworkIdentifierDetailed {
1291            ssid: unsaved_id.ssid.clone(),
1292            security_type: types::SecurityTypeDetailed::Wpa2Personal,
1293        };
1294        let saved_unseen_id = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1295        let seen_credential = Credential::None;
1296        let unseen_credential = Credential::Password(b"password".to_vec());
1297
1298        // Save the networks
1299        assert!(
1300            saved_networks
1301                .store(saved_seen_id.clone(), seen_credential.clone())
1302                .await
1303                .expect("Failed to save network")
1304                .is_none()
1305        );
1306        assert!(
1307            saved_networks
1308                .store(saved_unseen_id.clone(), unseen_credential.clone())
1309                .await
1310                .expect("Failed to save network")
1311                .is_none()
1312        );
1313
1314        // Record passive scan results, including the saved network and another network.
1315        let results: HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>> = HashMap::from([
1316            (
1317                saved_seen_network,
1318                vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1319            ),
1320            (unsaved_network, vec![generate_random_bss()]),
1321        ]);
1322
1323        saved_networks
1324            .record_scan_result(vec!["some_other_ssid".try_into().unwrap()], &results)
1325            .await;
1326
1327        assert_matches!(saved_networks.lookup(&saved_seen_id).await.as_slice(), [config] => {
1328            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1329        });
1330        assert_matches!(saved_networks.lookup(&saved_unseen_id).await.as_slice(), [config] => {
1331            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1332        });
1333    }
1334
1335    #[fuchsia::test]
1336    async fn test_record_undirected_scan_with_upgraded_security() {
1337        // Test that if we see a different compatible (higher) scan result for a saved network that
1338        // could be used to connect, recording the scan results will change the hidden probability.
1339        let saved_networks = SavedNetworksManager::new_for_test().await;
1340        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1341        let credential = Credential::Password(b"credential".to_vec());
1342
1343        // Save the networks
1344        assert!(
1345            saved_networks
1346                .store(id.clone(), credential.clone())
1347                .await
1348                .expect("Failed to save network")
1349                .is_none()
1350        );
1351
1352        // Record passive scan results
1353        let results = HashMap::from([(
1354            types::NetworkIdentifierDetailed {
1355                ssid: id.ssid.clone(),
1356                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1357            },
1358            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1359        )]);
1360        saved_networks.record_scan_result(vec![], &results).await;
1361        // The network was seen in a passive scan, so hidden probability should be updated.
1362        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1363            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1364        });
1365    }
1366
1367    #[fuchsia::test]
1368    async fn test_record_undirected_scan_incompatible_credential() {
1369        // Test that if we see a different compatible (higher) scan result for a saved network that
1370        // could be used to connect, recording the scan results will change the hidden probability.
1371        let saved_networks = SavedNetworksManager::new_for_test().await;
1372        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1373        let credential = Credential::Psk(vec![8; 32]);
1374
1375        // Save the networks
1376        assert!(
1377            saved_networks
1378                .store(id.clone(), credential.clone())
1379                .await
1380                .expect("Failed to save network")
1381                .is_none()
1382        );
1383
1384        // Record passive scan results, including the saved network and another network.
1385        let results = HashMap::from([(
1386            types::NetworkIdentifierDetailed {
1387                ssid: id.ssid.clone(),
1388                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1389            },
1390            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1391        )]);
1392        saved_networks.record_scan_result(vec![], &results).await;
1393        // The network in the passive scan results was not compatible, so hidden probability should
1394        // not have been updated.
1395        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1396            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1397        });
1398    }
1399
1400    #[fuchsia::test]
1401    async fn test_record_directed_scan_for_upgraded_security() {
1402        // Test that if we see a different compatible (higher) scan result for a saved network that
1403        // could be used to connect in a directed scan, the hidden probability will not be lowered.
1404        let saved_networks = SavedNetworksManager::new_for_test().await;
1405        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa).unwrap();
1406        let credential = Credential::Password(b"credential".to_vec());
1407
1408        // Save the networks
1409        assert!(
1410            saved_networks
1411                .store(id.clone(), credential.clone())
1412                .await
1413                .expect("Failed to save network")
1414                .is_none()
1415        );
1416        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1417        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1418
1419        // Record directed scan results. The config's probability hidden should not be lowered
1420        // since we did not fail to see it in a directed scan.
1421        let results = HashMap::from([(
1422            types::NetworkIdentifierDetailed {
1423                ssid: id.ssid.clone(),
1424                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1425            },
1426            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1427        )]);
1428        let target = vec![id.ssid.clone()];
1429        saved_networks.record_scan_result(target, &results).await;
1430
1431        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1432        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1433    }
1434
1435    #[fuchsia::test]
1436    async fn test_record_directed_scan_for_incompatible_credential() {
1437        // Test that if we see a network that is not compatible because of the saved credential
1438        // (but is otherwise compatible), the directed scan is not considered successful and the
1439        // hidden probability of the config is lowered.
1440        let saved_networks = SavedNetworksManager::new_for_test().await;
1441        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1442        let credential = Credential::Psk(vec![11; 32]);
1443
1444        // Save the networks
1445        assert!(
1446            saved_networks
1447                .store(id.clone(), credential.clone())
1448                .await
1449                .expect("Failed to save network")
1450                .is_none()
1451        );
1452        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1453        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1454
1455        // Record directed scan results. The seen network does not match the saved network even
1456        // though security is compatible, since the security type is not compatible with the PSK.
1457        let target = vec![id.ssid.clone()];
1458        let results = HashMap::from([(
1459            types::NetworkIdentifierDetailed {
1460                ssid: id.ssid.clone(),
1461                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1462            },
1463            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1464        )]);
1465        saved_networks.record_scan_result(target, &results).await;
1466        // The hidden probability should have been lowered because a directed scan failed to find
1467        // the network.
1468        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1469        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1470    }
1471
1472    #[fuchsia::test]
1473    async fn test_record_directed_scan_no_ssid_match() {
1474        // Test that recording directed active scan results does not mistakenly match a config with
1475        // a network with a different SSID.
1476
1477        let saved_networks = SavedNetworksManager::new_for_test().await;
1478        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1479        let credential = Credential::Psk(vec![11; 32]);
1480        let diff_ssid = types::Ssid::try_from("other-ssid").unwrap();
1481
1482        // Save the networks
1483        assert!(
1484            saved_networks
1485                .store(id.clone(), credential.clone())
1486                .await
1487                .expect("Failed to save network")
1488                .is_none()
1489        );
1490        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1491        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1492
1493        // Record directed scan results. We target the saved network but see a different one.
1494        let target = vec![id.ssid.clone()];
1495        let results = HashMap::from([(
1496            types::NetworkIdentifierDetailed {
1497                ssid: diff_ssid,
1498                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1499            },
1500            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1501        )]);
1502        saved_networks.record_scan_result(target, &results).await;
1503
1504        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1505        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1506    }
1507
1508    #[fuchsia::test]
1509    async fn test_record_directed_one_not_compatible_one_compatible() {
1510        // Test that if we see two networks with the same SSID but only one is compatible, the scan
1511        // is recorded as successful for the config. In other words it isn't mistakenly recorded as
1512        // a failure because of the config that isn't compatible.
1513        let saved_networks = SavedNetworksManager::new_for_test().await;
1514        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1515        let credential = Credential::Password(b"foo-pass".to_vec());
1516
1517        // Save the networks
1518        assert!(
1519            saved_networks
1520                .store(id.clone(), credential.clone())
1521                .await
1522                .expect("Failed to save network")
1523                .is_none()
1524        );
1525        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1526        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1527
1528        // Record directed scan results. We see one network with the same SSID that doesn't match,
1529        // and one that does match.
1530        let target = vec![id.ssid.clone()];
1531        let results = HashMap::from([
1532            (
1533                types::NetworkIdentifierDetailed {
1534                    ssid: id.ssid.clone(),
1535                    security_type: types::SecurityTypeDetailed::Wpa1,
1536                },
1537                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1538            ),
1539            (
1540                types::NetworkIdentifierDetailed {
1541                    ssid: id.ssid.clone(),
1542                    security_type: types::SecurityTypeDetailed::Wpa2Personal,
1543                },
1544                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1545            ),
1546        ]);
1547        saved_networks.record_scan_result(target, &results).await;
1548        // Since the directed scan found a matching network, the hidden probability should not
1549        // have been lowered.
1550        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1551        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1552    }
1553
1554    #[fuchsia::test]
1555    async fn test_record_both_directed_and_undirected() {
1556        let saved_networks = SavedNetworksManager::new_for_test().await;
1557        let saved_undirected_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1558        let saved_undirected_network = types::NetworkIdentifierDetailed {
1559            ssid: saved_undirected_id.ssid.clone(),
1560            security_type: types::SecurityTypeDetailed::Open,
1561        };
1562        let saved_directed_id = NetworkIdentifier::try_from("bar", SecurityType::None).unwrap();
1563        let credential = Credential::None;
1564
1565        // Save the networks
1566        assert!(
1567            saved_networks
1568                .store(saved_undirected_id.clone(), credential.clone())
1569                .await
1570                .expect("Failed to save network")
1571                .is_none()
1572        );
1573        assert!(
1574            saved_networks
1575                .store(saved_directed_id.clone(), credential.clone())
1576                .await
1577                .expect("Failed to save network")
1578                .is_none()
1579        );
1580
1581        // Verify assumption
1582        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1583            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1584        });
1585
1586        // Record scan results
1587        let results = HashMap::from([(
1588            saved_undirected_network,
1589            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1590        )]);
1591        saved_networks.record_scan_result(vec![saved_directed_id.ssid.clone()], &results).await;
1592
1593        // The undirected (but seen) network is modified
1594        assert_matches!(saved_networks.lookup(&saved_undirected_id).await.as_slice(), [config] => {
1595            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1596        });
1597        // The directed (but *not* seen) network is modified
1598        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1599            assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1600        });
1601    }
1602
1603    #[fuchsia::test]
1604    fn evict_if_needed_removes_unconnected() {
1605        // this test is less meaningful when MAX_CONFIGS_PER_SSID is greater than 1, otherwise
1606        // the only saved configs should be removed when the max capacity is met, regardless of
1607        // whether it has been connected to.
1608        let unconnected_config = network_config("foo", "password");
1609        let mut connected_config = unconnected_config.clone();
1610        connected_config.has_ever_connected = false;
1611        let mut network_configs = vec![connected_config; MAX_CONFIGS_PER_SSID - 1];
1612        network_configs.insert(MAX_CONFIGS_PER_SSID / 2, unconnected_config.clone());
1613
1614        assert_eq!(evict_if_needed(&mut network_configs), Some(unconnected_config));
1615        assert_eq!(MAX_CONFIGS_PER_SSID - 1, network_configs.len());
1616        // check that everything left has been connected to before, only one removed is
1617        // the one that has never been connected to
1618        for config in network_configs.iter() {
1619            assert!(config.has_ever_connected);
1620        }
1621    }
1622
1623    #[fuchsia::test]
1624    fn evict_if_needed_already_has_space() {
1625        let mut configs = vec![];
1626        assert_eq!(evict_if_needed(&mut configs), None);
1627        let expected_cfgs: Vec<NetworkConfig> = vec![];
1628        assert_eq!(expected_cfgs, configs);
1629
1630        if MAX_CONFIGS_PER_SSID > 1 {
1631            let mut configs = vec![network_config("foo", "password")];
1632            assert_eq!(evict_if_needed(&mut configs), None);
1633            // if MAX_CONFIGS_PER_SSID is 1, this wouldn't be true
1634            assert_eq!(vec![network_config("foo", "password")], configs);
1635        }
1636    }
1637
1638    #[fuchsia::test]
1639    async fn clear() {
1640        let store_id = "clear";
1641        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1642        let saved_networks = create_saved_networks(store_id).await;
1643
1644        assert!(
1645            saved_networks
1646                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
1647                .await
1648                .expect("storing 'foo' failed")
1649                .is_none()
1650        );
1651        assert_eq!(
1652            vec![network_config("foo", "qwertyuio")],
1653            saved_networks.lookup(&network_id).await
1654        );
1655        assert_eq!(1, saved_networks.known_network_count().await);
1656
1657        saved_networks.clear().await.expect("failed to clear saved networks");
1658        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1659        assert_eq!(0, saved_networks.known_network_count().await);
1660
1661        // Load store from storage to verify it is also gone from persistent storage
1662        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1663        let store = PolicyStorage::new_with_id(store_id).await;
1664        let saved_networks =
1665            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1666                .await;
1667
1668        assert_eq!(0, saved_networks.known_network_count().await);
1669    }
1670
1671    impl std::fmt::Debug for SavedNetworksManager {
1672        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1673            f.debug_struct("SavedNetworksManager")
1674                .field("saved_networks", &self.saved_networks)
1675                .finish()
1676        }
1677    }
1678
1679    #[fuchsia::test]
1680    fn test_store_errors_cause_write_errors() {
1681        use fidl::endpoints::create_request_stream;
1682        use fidl_fuchsia_stash as fidl_stash;
1683        use futures::StreamExt;
1684        use std::sync::Arc;
1685        use std::sync::atomic::{AtomicBool, Ordering};
1686
1687        // Use a path for the persistent store that will cause write errors.
1688        let store_path_str = "/////";
1689        let mut exec = fasync::TestExecutor::new();
1690
1691        // Initialize stash proxies such that SavedNetworksManager initialize doesn't wait on
1692        // and doesn't load anything from the legacy stash.
1693        let (stash_client, mut request_stream) =
1694            create_request_stream::<fidl_stash::SecureStoreMarker>();
1695
1696        let read_from_stash = Arc::new(AtomicBool::new(false));
1697
1698        let _task = {
1699            let read_from_stash = read_from_stash.clone();
1700            fasync::Task::local(async move {
1701                while let Some(request) = request_stream.next().await {
1702                    match request.unwrap() {
1703                        fidl_stash::SecureStoreRequest::Identify { .. } => {}
1704                        fidl_stash::SecureStoreRequest::CreateAccessor {
1705                            accessor_request, ..
1706                        } => {
1707                            let read_from_stash = read_from_stash.clone();
1708                            fuchsia_async::EHandle::local().spawn_detached(async move {
1709                                let mut request_stream = accessor_request.into_stream();
1710                                while let Some(request) = request_stream.next().await {
1711                                    match request.unwrap() {
1712                                        fidl_stash::StoreAccessorRequest::ListPrefix { .. } => {
1713                                            read_from_stash.store(true, Ordering::Relaxed);
1714                                            // If we just drop the iterator, it should trigger a
1715                                            // read error.
1716                                        }
1717                                        _ => unreachable!(),
1718                                    }
1719                                }
1720                            });
1721                        }
1722                    }
1723                }
1724            })
1725        };
1726
1727        // Use a persistent store with the invalid file name and legacy stash which returns errors.
1728        let store =
1729            PolicyStorage::new_with_stash_proxy_and_id(stash_client.into_proxy(), store_path_str);
1730
1731        // Initialize the saved networks manager with the file that should cause write errors, the
1732        // legacy stash that will load nothing, and empty legacy known ess store file.
1733        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1734        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1735        let init_fut = SavedNetworksManager::new_with_storage(store, telemetry_sender);
1736        let mut init_fut = pin!(init_fut);
1737        let saved_networks = assert_matches!(exec.run_until_stalled(&mut init_fut), Poll::Ready(snm) => {
1738            snm
1739        });
1740
1741        // Save and remove networks and check that we get storage write errors
1742        let ssid = "foo";
1743        let credential = Credential::None;
1744        let network_id = NetworkIdentifier::try_from(ssid, SecurityType::None).unwrap();
1745        let save_fut = saved_networks.store(network_id.clone(), credential);
1746        let mut save_fut = pin!(save_fut);
1747
1748        assert_matches!(
1749            exec.run_until_stalled(&mut save_fut),
1750            Poll::Ready(Err(NetworkConfigError::FileWriteError))
1751        );
1752
1753        // The network should have been saved temporarily even if saving the network gives an error.
1754        assert_matches!(exec.run_until_stalled(&mut saved_networks.lookup(&network_id)), Poll::Ready(configs) => {
1755            assert_eq!(configs, vec![network_config(ssid, "")]);
1756        });
1757        assert_matches!(exec.run_until_stalled(&mut saved_networks.known_network_count()), Poll::Ready(count) => {
1758            assert_eq!(count, 1);
1759        });
1760    }
1761
1762    /// Create a saved networks manager and clear the contents. Storage ID should be different for
1763    /// each test so that they don't interfere.
1764    async fn create_saved_networks(store_id: &str) -> SavedNetworksManager {
1765        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1766        let store = PolicyStorage::new_with_id(store_id).await;
1767        let saved_networks =
1768            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1769                .await;
1770        saved_networks.clear().await.expect("failed to clear saved networks");
1771        saved_networks
1772    }
1773
1774    /// Convience function for creating network configs with default values as they would be
1775    /// initialized when read from KnownEssStore. Credential is password or none, and security
1776    /// type is WPA2 or none.
1777    fn network_config(ssid: &str, password: impl Into<Vec<u8>>) -> NetworkConfig {
1778        let credential = Credential::from_bytes(password.into());
1779        let id = NetworkIdentifier::try_from(ssid, credential.derived_security_type()).unwrap();
1780        let has_ever_connected = false;
1781        NetworkConfig::new(id, credential, has_ever_connected).unwrap()
1782    }
1783
1784    #[fuchsia::test]
1785    async fn record_metrics_when_called_on_class() {
1786        let store_id = generate_string();
1787        let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1788        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1789        let store = PolicyStorage::new_with_id(&store_id).await;
1790
1791        let saved_networks = SavedNetworksManager::new_with_storage(store, telemetry_sender).await;
1792        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1793        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1794
1795        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
1796        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1797        assert_eq!(0, saved_networks.known_network_count().await);
1798
1799        // Store a network and verify it was stored.
1800        assert!(
1801            saved_networks
1802                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
1803                .await
1804                .expect("storing 'foo' failed")
1805                .is_none()
1806        );
1807        assert_eq!(1, saved_networks.known_network_count().await);
1808
1809        // Store another network and verify.
1810        assert!(
1811            saved_networks
1812                .store(network_id_baz.clone(), Credential::Psk(vec![1; 32]))
1813                .await
1814                .expect("storing 'baz' with PSK failed")
1815                .is_none()
1816        );
1817        assert_eq!(2, saved_networks.known_network_count().await);
1818
1819        // Record metrics
1820        saved_networks.record_periodic_metrics().await;
1821
1822        // Verify metric is logged with two saved networks, which each have one config
1823        assert_matches!(telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::SavedNetworkCount { saved_network_count, config_count_per_saved_network })) => {
1824            assert_eq!(saved_network_count, 2);
1825            assert_eq!(config_count_per_saved_network, [1, 1]);
1826        });
1827    }
1828
1829    #[fuchsia::test]
1830    async fn probabilistic_choosing_of_hidden_networks() {
1831        // Create three networks with 1, 0, 0.5 hidden probability
1832        let id_hidden = types::NetworkIdentifier {
1833            ssid: types::Ssid::try_from("hidden").unwrap(),
1834            security_type: types::SecurityType::Wpa2,
1835        };
1836        let mut net_config_hidden = NetworkConfig::new(
1837            id_hidden.clone(),
1838            Credential::Password(b"password".to_vec()),
1839            false,
1840        )
1841        .expect("failed to create network config");
1842        net_config_hidden.hidden_probability = 1.0;
1843
1844        let id_not_hidden = types::NetworkIdentifier {
1845            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1846            security_type: types::SecurityType::Wpa2,
1847        };
1848        let mut net_config_not_hidden = NetworkConfig::new(
1849            id_not_hidden.clone(),
1850            Credential::Password(b"password".to_vec()),
1851            false,
1852        )
1853        .expect("failed to create network config");
1854        net_config_not_hidden.hidden_probability = 0.0;
1855
1856        let id_maybe_hidden = types::NetworkIdentifier {
1857            ssid: types::Ssid::try_from("maybe_hidden").unwrap(),
1858            security_type: types::SecurityType::Wpa2,
1859        };
1860        let mut net_config_maybe_hidden = NetworkConfig::new(
1861            id_maybe_hidden.clone(),
1862            Credential::Password(b"password".to_vec()),
1863            false,
1864        )
1865        .expect("failed to create network config");
1866        net_config_maybe_hidden.hidden_probability = 0.5;
1867
1868        let mut maybe_hidden_selection_count = 0;
1869        let mut hidden_selection_count = 0;
1870
1871        // Run selection many times, to ensure the probability is working as expected.
1872        for _ in 1..100 {
1873            let selected_networks = select_subset_potentially_hidden_networks(vec![
1874                net_config_hidden.clone(),
1875                net_config_not_hidden.clone(),
1876                net_config_maybe_hidden.clone(),
1877            ]);
1878            // The 1.0 probability should always be picked
1879            assert!(selected_networks.contains(&id_hidden));
1880            // The 0 probability should never be picked
1881            assert!(!selected_networks.contains(&id_not_hidden));
1882
1883            // Keep track of how often the networks were selected
1884            if selected_networks.contains(&id_maybe_hidden) {
1885                maybe_hidden_selection_count += 1;
1886            }
1887            if selected_networks.contains(&id_hidden) {
1888                hidden_selection_count += 1;
1889            }
1890        }
1891
1892        // The 0.5 probability network should be picked at least once, but not every time. With 100
1893        // runs, the chances of either of these assertions flaking is 1 / (0.5^100), i.e. 1 in 1e30.
1894        // Even with a hypothetical 1,000,000 test runs per day, there would be an average of 1e24
1895        // days between flakes due to this test.
1896        assert!(maybe_hidden_selection_count > 0);
1897        assert!(maybe_hidden_selection_count < hidden_selection_count);
1898    }
1899
1900    #[fuchsia::test]
1901    async fn test_select_high_probability_hidden_networks() {
1902        // Create three networks with 1, 0, 0.5 hidden probability
1903        let id_hidden = types::NetworkIdentifier {
1904            ssid: types::Ssid::try_from("hidden").unwrap(),
1905            security_type: types::SecurityType::Wpa2,
1906        };
1907        let mut net_config_hidden = NetworkConfig::new(
1908            id_hidden.clone(),
1909            Credential::Password(b"password".to_vec()),
1910            false,
1911        )
1912        .expect("failed to create network config");
1913        net_config_hidden.hidden_probability = 1.0;
1914
1915        let id_maybe_hidden_high = types::NetworkIdentifier {
1916            ssid: types::Ssid::try_from("maybe_hidden_high").unwrap(),
1917            security_type: types::SecurityType::Wpa2,
1918        };
1919        let mut net_config_maybe_hidden_high = NetworkConfig::new(
1920            id_maybe_hidden_high.clone(),
1921            Credential::Password(b"password".to_vec()),
1922            false,
1923        )
1924        .expect("failed to create network config");
1925        net_config_maybe_hidden_high.hidden_probability = 0.8;
1926
1927        let id_maybe_hidden_low = types::NetworkIdentifier {
1928            ssid: types::Ssid::try_from("maybe_hidden_low").unwrap(),
1929            security_type: types::SecurityType::Wpa2,
1930        };
1931        let mut net_config_maybe_hidden_low = NetworkConfig::new(
1932            id_maybe_hidden_low.clone(),
1933            Credential::Password(b"password".to_vec()),
1934            false,
1935        )
1936        .expect("failed to create network config");
1937        net_config_maybe_hidden_low.hidden_probability = 0.7;
1938
1939        let id_not_hidden = types::NetworkIdentifier {
1940            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1941            security_type: types::SecurityType::Wpa2,
1942        };
1943        let mut net_config_not_hidden = NetworkConfig::new(
1944            id_not_hidden.clone(),
1945            Credential::Password(b"password".to_vec()),
1946            false,
1947        )
1948        .expect("failed to create network config");
1949        net_config_not_hidden.hidden_probability = 0.0;
1950
1951        let selected_networks = select_high_probability_hidden_networks(vec![
1952            net_config_hidden.clone(),
1953            net_config_maybe_hidden_high.clone(),
1954            net_config_maybe_hidden_low.clone(),
1955            net_config_not_hidden.clone(),
1956        ]);
1957
1958        // The 1.0 probability should always be picked
1959        assert!(selected_networks.contains(&id_hidden));
1960        // The high probability should always be picked
1961        assert!(selected_networks.contains(&id_maybe_hidden_high));
1962        // The low probability should never be picked
1963        assert!(!selected_networks.contains(&id_maybe_hidden_low));
1964        // The 0 probability should never be picked
1965        assert!(!selected_networks.contains(&id_not_hidden));
1966    }
1967
1968    #[fuchsia::test]
1969    async fn test_record_not_seen_active_scan() {
1970        // Test that if we update that we haven't seen a couple of networks in active scans, their
1971        // hidden probability is updated.
1972        let saved_networks = SavedNetworksManager::new_for_test().await;
1973
1974        // Seen in active scans
1975        let id_1 = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
1976        let credential_1 = Credential::Password(b"some_password".to_vec());
1977        let id_2 = NetworkIdentifier::try_from("bar", SecurityType::Wpa3).unwrap();
1978        let credential_2 = Credential::Password(b"another_password".to_vec());
1979        // Seen in active scan but not saved
1980        let id_3 = NetworkIdentifier::try_from("baz", SecurityType::None).unwrap();
1981        // Saved and targeted in active scan but not seen
1982        let id_4 = NetworkIdentifier::try_from("foobar", SecurityType::None).unwrap();
1983        let credential_4 = Credential::None;
1984
1985        // Save 3 of the 4 networks
1986        assert!(
1987            saved_networks
1988                .store(id_1.clone(), credential_1)
1989                .await
1990                .expect("failed to store network")
1991                .is_none()
1992        );
1993        assert!(
1994            saved_networks
1995                .store(id_2.clone(), credential_2)
1996                .await
1997                .expect("failed to store network")
1998                .is_none()
1999        );
2000        assert!(
2001            saved_networks
2002                .store(id_4.clone(), credential_4)
2003                .await
2004                .expect("failed to store network")
2005                .is_none()
2006        );
2007        // Check that the saved networks have the default hidden probability so later we can just
2008        // check that the probability has changed.
2009        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2010        assert_eq!(config_1.hidden_probability, PROB_HIDDEN_DEFAULT);
2011        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2012        assert_eq!(config_2.hidden_probability, PROB_HIDDEN_DEFAULT);
2013        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2014        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2015
2016        let not_seen_ids = vec![id_1.ssid.clone(), id_2.ssid.clone(), id_3.ssid.clone()];
2017        saved_networks.record_scan_result(not_seen_ids, &HashMap::new()).await;
2018
2019        // Check that the configs' hidden probability has decreased
2020        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2021        assert!(config_1.hidden_probability < PROB_HIDDEN_DEFAULT);
2022        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2023        assert!(config_2.hidden_probability < PROB_HIDDEN_DEFAULT);
2024
2025        // Check that for the network that was target but not seen in the active scan, its hidden
2026        // probability isn't lowered.
2027        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2028        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2029
2030        // Check that a config was not saved for the identifier that was not saved before.
2031        assert!(saved_networks.lookup(&id_3).await.is_empty());
2032    }
2033
2034    #[fuchsia::test]
2035    async fn test_update_scan_stats_for_single_bss() {
2036        // Record multiple scans for a network where there is only 1 BSS for the network. The
2037        // config should be considered likely single BSS.
2038        let saved_networks = SavedNetworksManager::new_for_test().await;
2039
2040        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2041        let credential = Credential::Password(b"some_password".to_vec());
2042        assert!(
2043            saved_networks
2044                .store(id.clone(), credential.clone())
2045                .await
2046                .expect("failed to store network")
2047                .is_none()
2048        );
2049
2050        let id_detailed = types::NetworkIdentifierDetailed {
2051            ssid: id.ssid.clone(),
2052            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2053        };
2054        let scan_results = HashMap::from([(
2055            id_detailed.clone(),
2056            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2057        )]);
2058
2059        // likely has one BSS
2060        for _ in 0..5 {
2061            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2062        }
2063
2064        let is_single_bss = saved_networks
2065            .is_network_single_bss(&id, &credential)
2066            .await
2067            .expect("failed to lookup if network is single BSS");
2068        assert!(is_single_bss);
2069    }
2070
2071    #[fuchsia::test]
2072    async fn test_update_scan_stats_for_multiple_bss_at_least_once() {
2073        // Record multiple scans for a network where there are multiple BSS. The network config
2074        // should say that the network is not single BSS.
2075        let saved_networks = SavedNetworksManager::new_for_test().await;
2076
2077        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2078        let credential = Credential::Password(b"some_password".to_vec());
2079        assert!(
2080            saved_networks
2081                .store(id.clone(), credential.clone())
2082                .await
2083                .expect("failed to store network")
2084                .is_none()
2085        );
2086
2087        let id_detailed = types::NetworkIdentifierDetailed {
2088            ssid: id.ssid.clone(),
2089            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2090        };
2091        let scan_results_single = HashMap::from([(
2092            id_detailed.clone(),
2093            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2094        )]);
2095
2096        let scan_results_multi = HashMap::from([(
2097            id_detailed.clone(),
2098            vec![
2099                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2100                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2101            ],
2102        )]);
2103
2104        // Record some scan results with one BSS, and record once with multiple BSS.
2105        for _ in 0..2 {
2106            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2107        }
2108
2109        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_multi).await;
2110        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2111
2112        // The one scan with multiple BSS results should make the network determined to be
2113        // multi BSS.
2114        let is_single_bss = saved_networks
2115            .is_network_single_bss(&id, &credential)
2116            .await
2117            .expect("failed to lookup if network is single BSS");
2118        assert!(!is_single_bss);
2119    }
2120
2121    #[fuchsia::test]
2122    async fn test_record_scan_more_than_once_to_decide_single_bss() {
2123        // Test that a network is not decided to be single BSS after only one scan.
2124        let saved_networks = SavedNetworksManager::new_for_test().await;
2125
2126        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2127        let credential = Credential::Password(b"some_password".to_vec());
2128        assert!(
2129            saved_networks
2130                .store(id.clone(), credential.clone())
2131                .await
2132                .expect("failed to store network")
2133                .is_none()
2134        );
2135
2136        let id_detailed = types::NetworkIdentifierDetailed {
2137            ssid: id.ssid.clone(),
2138            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2139        };
2140        let scan_results = HashMap::from([(
2141            id_detailed,
2142            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2143        )]);
2144
2145        // Record the scan multiple times, since multiple scans are needed to decide the network
2146        // likely has one BSS
2147        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2148
2149        let is_single_bss = saved_networks
2150            .is_network_single_bss(&id, &credential)
2151            .await
2152            .expect("failed to lookup if network is single BSS");
2153        assert!(!is_single_bss);
2154    }
2155
2156    #[fuchsia::test]
2157    async fn test_get_past_connections() {
2158        let saved_networks_manager = SavedNetworksManager::new_for_test().await;
2159
2160        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2161        let credential = Credential::Password(b"some_password".to_vec());
2162        let mut config = NetworkConfig::new(id.clone(), credential.clone(), true)
2163            .expect("failed to create config");
2164        let mut past_connections = HistoricalListsByBssid::new();
2165
2166        // Add two past connections with the same bssid
2167        let data_1 = random_connection_data();
2168        let bssid_1 = data_1.bssid;
2169        let mut data_2 = random_connection_data();
2170        data_2.bssid = bssid_1;
2171        past_connections.add(bssid_1, data_1);
2172        past_connections.add(bssid_1, data_2);
2173
2174        // Add a past connection with different bssid
2175        let data_3 = random_connection_data();
2176        let bssid_2 = data_3.bssid;
2177        past_connections.add(bssid_2, data_3);
2178        config.perf_stats.past_connections = past_connections;
2179
2180        // Create SavedNetworksManager with configs that have past connections
2181        assert!(
2182            saved_networks_manager
2183                .saved_networks
2184                .lock()
2185                .await
2186                .insert(id.clone(), vec![config])
2187                .is_none()
2188        );
2189
2190        // Check that get_past_connections gets the two PastConnectionLists for the BSSIDs.
2191        let mut expected_past_connections = PastConnectionList::default();
2192        expected_past_connections.add(data_1);
2193        expected_past_connections.add(data_2);
2194        let actual_past_connections =
2195            saved_networks_manager.get_past_connections(&id, &credential, &bssid_1).await;
2196        assert_eq!(actual_past_connections, expected_past_connections);
2197
2198        let mut expected_past_connections = PastConnectionList::default();
2199        expected_past_connections.add(data_3);
2200        let actual_past_connections =
2201            saved_networks_manager.get_past_connections(&id, &credential, &bssid_2).await;
2202        assert_eq!(actual_past_connections, expected_past_connections);
2203
2204        // Check that get_past_connections will not get the PastConnectionLists if the specified
2205        // Credential is different.
2206        let actual_past_connections = saved_networks_manager
2207            .get_past_connections(&id, &Credential::Password(b"other-password".to_vec()), &bssid_1)
2208            .await;
2209        assert_eq!(actual_past_connections, PastConnectionList::default());
2210    }
2211
2212    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
2213        fidl_sme::ConnectResult {
2214            code: fidl_ieee80211::StatusCode::Success,
2215            is_credential_rejected: false,
2216            is_reconnect: false,
2217        }
2218    }
2219}