Skip to main content

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