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