wlancfg_lib/client/connection_selection/
mod.rs

1// Copyright 2023 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 crate::client::scan::{self, ScanReason};
6use crate::client::types::{
7    self, Bss, InternalSavedNetworkData, SecurityType, SecurityTypeDetailed,
8};
9use crate::config_management::{
10    self, Credential, SavedNetworksManagerApi, network_config, select_authentication_method,
11    select_subset_potentially_hidden_networks,
12};
13use crate::telemetry::{self, TelemetryEvent, TelemetrySender};
14use anyhow::format_err;
15use async_trait::async_trait;
16use fuchsia_inspect::Node as InspectNode;
17use fuchsia_inspect_auto_persist::{self as auto_persist, AutoPersist};
18use fuchsia_inspect_contrib::inspect_insert;
19use fuchsia_inspect_contrib::log::WriteInspect;
20use fuchsia_inspect_contrib::nodes::BoundedListNode as InspectBoundedListNode;
21use futures::channel::{mpsc, oneshot};
22use futures::lock::Mutex;
23use futures::select;
24use futures::stream::StreamExt;
25use log::{debug, error, info, warn};
26use std::borrow::Cow;
27use std::collections::{HashMap, HashSet};
28use std::rc::Rc;
29use std::sync::Arc;
30use wlan_common::security::SecurityAuthenticator;
31use wlan_common::sequestered::Sequestered;
32use {fidl_fuchsia_wlan_common as fidl_common, fuchsia_async as fasync};
33
34pub mod bss_selection;
35pub mod network_selection;
36pub mod scoring_functions;
37
38pub const CONNECTION_SELECTION_REQUEST_BUFFER_SIZE: usize = 100;
39const INSPECT_EVENT_LIMIT_FOR_CONNECTION_SELECTIONS: usize = 10;
40
41const RECENT_DISCONNECT_WINDOW: zx::MonotonicDuration =
42    zx::MonotonicDuration::from_seconds(60 * 15);
43const RECENT_FAILURE_WINDOW: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(60 * 5);
44const SHORT_CONNECT_DURATION: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(7 * 60);
45
46#[derive(Clone)]
47pub struct ConnectionSelectionRequester {
48    sender: mpsc::Sender<ConnectionSelectionRequest>,
49}
50
51impl ConnectionSelectionRequester {
52    pub fn new(sender: mpsc::Sender<ConnectionSelectionRequest>) -> Self {
53        Self { sender }
54    }
55
56    pub async fn do_connection_selection(
57        &mut self,
58        // None if no specific SSID is requested.
59        network_id: Option<types::NetworkIdentifier>,
60        reason: types::ConnectReason,
61    ) -> Result<Option<types::ScannedCandidate>, anyhow::Error> {
62        let (sender, receiver) = oneshot::channel();
63        self.sender
64            .try_send(ConnectionSelectionRequest::NewConnectionSelection {
65                network_id,
66                reason,
67                responder: sender,
68            })
69            .map_err(|e| format_err!("Failed to send connection selection request: {}", e))?;
70        receiver.await.map_err(|e| format_err!("Error during connection selection: {:?}", e))
71    }
72    pub async fn do_roam_selection(
73        &mut self,
74        scan_type: fidl_common::ScanType,
75        network_id: types::NetworkIdentifier,
76        credential: network_config::Credential,
77        current_security: types::SecurityTypeDetailed,
78    ) -> Result<Option<types::ScannedCandidate>, anyhow::Error> {
79        let (sender, receiver) = oneshot::channel();
80        self.sender
81            .try_send(ConnectionSelectionRequest::RoamSelection {
82                scan_type,
83                network_id,
84                credential,
85                current_security,
86                responder: sender,
87            })
88            .map_err(|e| format_err!("Failed to queue connection selection: {}", e))?;
89        receiver.await.map_err(|e| format_err!("Error during roam selection: {:?}", e))
90    }
91}
92
93#[cfg_attr(test, derive(Debug))]
94pub enum ConnectionSelectionRequest {
95    NewConnectionSelection {
96        network_id: Option<types::NetworkIdentifier>,
97        reason: types::ConnectReason,
98        responder: oneshot::Sender<Option<types::ScannedCandidate>>,
99    },
100    RoamSelection {
101        scan_type: fidl_common::ScanType,
102        network_id: types::NetworkIdentifier,
103        credential: network_config::Credential,
104        current_security: types::SecurityTypeDetailed,
105        responder: oneshot::Sender<Option<types::ScannedCandidate>>,
106    },
107}
108
109// Primary functionality in a trait, so it can be stubbed for tests.
110#[async_trait(?Send)]
111pub trait ConnectionSelectorApi {
112    // Find best connection candidate to initialize a connection.
113    async fn find_and_select_connection_candidate(
114        &self,
115        network: Option<types::NetworkIdentifier>,
116        reason: types::ConnectReason,
117    ) -> Option<types::ScannedCandidate>;
118    // Find best roam candidate.
119    async fn find_and_select_roam_candidate(
120        &self,
121        scan_type: fidl_common::ScanType,
122        network: types::NetworkIdentifier,
123        credential: &network_config::Credential,
124        current_security: types::SecurityTypeDetailed,
125    ) -> Option<types::ScannedCandidate>;
126}
127
128// Main loop to handle incoming requests.
129pub async fn serve_connection_selection_request_loop(
130    connection_selector: Rc<dyn ConnectionSelectorApi>,
131    mut request_channel: mpsc::Receiver<ConnectionSelectionRequest>,
132) {
133    loop {
134        select! {
135            request = request_channel.select_next_some() => {
136                match request {
137                    ConnectionSelectionRequest::NewConnectionSelection { network_id, reason, responder} => {
138                        let selected = connection_selector.find_and_select_connection_candidate(network_id, reason).await;
139                        // It's acceptable for the receiver to close the channel, preventing this
140                        // sender from responding.
141                        let _ = responder.send(selected);
142                    }
143                    ConnectionSelectionRequest::RoamSelection { scan_type, network_id, credential, current_security, responder } => {
144                        let selected = connection_selector.find_and_select_roam_candidate(scan_type, network_id, &credential, current_security).await;
145                        // It's acceptable for the receiver to close the channel, preventing this
146                        // sender from responding.
147                        let _ = responder.send(selected);
148                    }
149                }
150            }
151        }
152    }
153}
154
155pub struct ConnectionSelector {
156    saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
157    scan_requester: Arc<dyn scan::ScanRequestApi>,
158    last_scan_result_time: Arc<Mutex<zx::MonotonicInstant>>,
159    _inspect_node_root: Arc<Mutex<InspectNode>>,
160    inspect_node_for_connection_selection: Arc<Mutex<AutoPersist<InspectBoundedListNode>>>,
161    telemetry_sender: TelemetrySender,
162}
163
164impl ConnectionSelector {
165    pub fn new(
166        saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
167        scan_requester: Arc<dyn scan::ScanRequestApi>,
168        inspect_node: InspectNode,
169        persistence_req_sender: auto_persist::PersistenceReqSender,
170        telemetry_sender: TelemetrySender,
171    ) -> Self {
172        let inspect_node_for_connection_selection = InspectBoundedListNode::new(
173            inspect_node.create_child("connection_selection"),
174            INSPECT_EVENT_LIMIT_FOR_CONNECTION_SELECTIONS,
175        );
176        let inspect_node_for_connection_selection = AutoPersist::new(
177            inspect_node_for_connection_selection,
178            "wlancfg-network-selection",
179            persistence_req_sender,
180        );
181        Self {
182            saved_network_manager,
183            scan_requester,
184            last_scan_result_time: Arc::new(Mutex::new(zx::MonotonicInstant::ZERO)),
185            _inspect_node_root: Arc::new(Mutex::new(inspect_node)),
186            inspect_node_for_connection_selection: Arc::new(Mutex::new(
187                inspect_node_for_connection_selection,
188            )),
189            telemetry_sender,
190        }
191    }
192
193    /// Requests scans and compiles list of BSSs that appear in scan results and belong to currently
194    /// saved networks.
195    async fn find_available_bss_candidate_list(
196        &self,
197        network: Option<types::NetworkIdentifier>,
198    ) -> Vec<types::ScannedCandidate> {
199        let scan_for_candidates = || async {
200            if let Some(network) = &network {
201                self.scan_requester
202                    .perform_scan(ScanReason::BssSelection, vec![network.ssid.clone()], vec![])
203                    .await
204            } else {
205                let last_scan_result_time = *self.last_scan_result_time.lock().await;
206                let scan_age = zx::MonotonicInstant::get() - last_scan_result_time;
207                if last_scan_result_time != zx::MonotonicInstant::ZERO {
208                    info!("Scan results are {}s old, triggering a scan", scan_age.into_seconds());
209                    self.telemetry_sender.send(TelemetryEvent::NetworkSelectionScanInterval {
210                        time_since_last_scan: scan_age,
211                    });
212                }
213                let passive_scan_results = match self
214                    .scan_requester
215                    .perform_scan(ScanReason::NetworkSelection, vec![], vec![])
216                    .await
217                {
218                    Ok(scan_results) => scan_results,
219                    Err(e) => return Err(e),
220                };
221                let passive_scan_ssids: HashSet<types::Ssid> = HashSet::from_iter(
222                    passive_scan_results.iter().map(|result| result.ssid.clone()),
223                );
224                let requested_active_scan_ssids: Vec<types::Ssid> =
225                    select_subset_potentially_hidden_networks(
226                        self.saved_network_manager.get_networks().await,
227                    )
228                    .drain(..)
229                    .map(|id| id.ssid)
230                    .filter(|ssid| !passive_scan_ssids.contains(ssid))
231                    .collect();
232
233                self.telemetry_sender.send(TelemetryEvent::ActiveScanRequested {
234                    num_ssids_requested: requested_active_scan_ssids.len(),
235                });
236
237                if requested_active_scan_ssids.is_empty() {
238                    Ok(passive_scan_results)
239                } else {
240                    self.scan_requester
241                        .perform_scan(
242                            ScanReason::NetworkSelection,
243                            requested_active_scan_ssids,
244                            vec![],
245                        )
246                        .await
247                        .map(|mut scan_results| {
248                            scan_results.extend(passive_scan_results);
249                            scan_results
250                        })
251                }
252            }
253        };
254
255        let scan_results = scan_for_candidates().await;
256
257        match scan_results {
258            Err(e) => {
259                warn!("Failed to get available BSSs, {:?}", e);
260                vec![]
261            }
262            Ok(scan_results) => {
263                let candidates =
264                    merge_saved_networks_and_scan_data(&self.saved_network_manager, scan_results)
265                        .await;
266                if network.is_none() {
267                    *self.last_scan_result_time.lock().await = zx::MonotonicInstant::get();
268                    record_metrics_on_scan(candidates.clone(), &self.telemetry_sender);
269                }
270                candidates
271            }
272        }
273    }
274
275    /// If a BSS was discovered via a passive scan, we need to perform an active scan on it to
276    /// discover all the information potentially needed by the SME layer.
277    async fn augment_bss_candidate_with_active_scan(
278        &self,
279        scanned_candidate: types::ScannedCandidate,
280    ) -> types::ScannedCandidate {
281        // This internal function encapsulates all the logic and has a Result<> return type,
282        // allowing us to use the `?` operator inside it to reduce nesting.
283        async fn get_enhanced_bss_description(
284            scanned_candidate: &types::ScannedCandidate,
285            scan_requester: Arc<dyn scan::ScanRequestApi>,
286        ) -> Result<Sequestered<fidl_common::BssDescription>, ()> {
287            match scanned_candidate.bss.observation {
288                types::ScanObservation::Passive => {
289                    info!("Performing directed active scan on selected network")
290                }
291                types::ScanObservation::Active => {
292                    debug!("Network already discovered via active scan.");
293                    return Err(());
294                }
295                types::ScanObservation::Unknown => {
296                    error!("Unexpected `Unknown` variant of network `observation`.");
297                    return Err(());
298                }
299            }
300
301            // Perform the scan
302            let mut directed_scan_result = scan_requester
303                .perform_scan(
304                    ScanReason::BssSelectionAugmentation,
305                    vec![scanned_candidate.network.ssid.clone()],
306                    vec![scanned_candidate.bss.channel],
307                )
308                .await
309                .map_err(|_| {
310                    info!("Failed to perform active scan to augment BSS info.");
311                })?;
312
313            // Find the bss in the results
314            let bss_description = directed_scan_result
315                .drain(..)
316                .find_map(|mut network| {
317                    if network.ssid == scanned_candidate.network.ssid {
318                        for bss in network.entries.drain(..) {
319                            if bss.bssid == scanned_candidate.bss.bssid {
320                                return Some(bss.bss_description);
321                            }
322                        }
323                    }
324                    None
325                })
326                .ok_or_else(|| {
327                    info!("BSS info will lack active scan augmentation, proceeding anyway.");
328                })?;
329
330            Ok(bss_description)
331        }
332
333        match get_enhanced_bss_description(&scanned_candidate, self.scan_requester.clone()).await {
334            Ok(new_bss_description) => {
335                let updated_scanned_bss =
336                    Bss { bss_description: new_bss_description, ..scanned_candidate.bss.clone() };
337                types::ScannedCandidate { bss: updated_scanned_bss, ..scanned_candidate }
338            }
339            Err(()) => scanned_candidate,
340        }
341    }
342
343    // Find scan results in provided network
344    async fn roam_scan(
345        &self,
346        scan_type: fidl_common::ScanType,
347        network: types::NetworkIdentifier,
348        current_security: types::SecurityTypeDetailed,
349    ) -> Vec<types::ScanResult> {
350        let ssids = match scan_type {
351            fidl_common::ScanType::Passive => vec![],
352            fidl_common::ScanType::Active => vec![network.ssid.clone()],
353        };
354        self.scan_requester
355            .perform_scan(ScanReason::RoamSearch, ssids, vec![])
356            .await
357            .unwrap_or_else(|e| {
358                error!("{}", format_err!("Error scanning: {:?}", e));
359                vec![]
360            })
361            .into_iter()
362            .filter(|s| {
363                // Roaming is only allowed between APs with identical protection
364                s.ssid == network.ssid && current_security == s.security_type_detailed
365            })
366            .collect::<Vec<_>>()
367    }
368}
369
370#[async_trait(?Send)]
371impl ConnectionSelectorApi for ConnectionSelector {
372    /// Full connection selection. Scans to find available candidates, uses network selection (or
373    /// optional provided network) to filter out networks, and then bss selection to select the best
374    /// of the remaining candidates. If the candidate was discovered via a passive scan, augments the
375    /// bss info with an active scan.
376    async fn find_and_select_connection_candidate(
377        &self,
378        network: Option<types::NetworkIdentifier>,
379        reason: types::ConnectReason,
380    ) -> Option<types::ScannedCandidate> {
381        // Scan for BSSs belonging to saved networks.
382        let available_candidate_list =
383            self.find_available_bss_candidate_list(network.clone()).await;
384
385        // Network selection.
386        let available_networks: HashSet<types::NetworkIdentifier> =
387            available_candidate_list.iter().map(|candidate| candidate.network.clone()).collect();
388        let selected_networks = network_selection::select_networks(available_networks, &network);
389
390        // Send network selection metrics
391        self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
392            network_selection_type: match network {
393                Some(_) => telemetry::NetworkSelectionType::Directed,
394                None => telemetry::NetworkSelectionType::Undirected,
395            },
396            num_candidates: (!available_candidate_list.is_empty())
397                .then_some(available_candidate_list.len())
398                .ok_or(()),
399            selected_count: selected_networks.len(),
400        });
401
402        // Filter down to only BSSs in the selected networks.
403        let allowed_candidate_list = available_candidate_list
404            .iter()
405            .filter(|candidate| selected_networks.contains(&candidate.network))
406            .cloned()
407            .collect();
408
409        // BSS selection.
410
411        match bss_selection::select_bss(
412            allowed_candidate_list,
413            reason,
414            self.inspect_node_for_connection_selection.clone(),
415            self.telemetry_sender.clone(),
416        )
417        .await
418        {
419            Some(mut candidate) => {
420                if network.is_some() {
421                    // Strip scan observation type, because the candidate was discovered via a
422                    // directed active scan, so we cannot know if it is discoverable via a passive
423                    // scan.
424                    candidate.bss.observation = types::ScanObservation::Unknown;
425                }
426                // If it was observed passively, augment with active scan.
427                match candidate.bss.observation {
428                    types::ScanObservation::Passive => {
429                        Some(self.augment_bss_candidate_with_active_scan(candidate.clone()).await)
430                    }
431                    _ => Some(candidate),
432                }
433            }
434            None => None,
435        }
436    }
437
438    /// Return the "best" AP to connect to from the current network. It may be the same AP that is
439    /// currently connected. Returning None means that no APs were seen. The credential is required
440    /// to ensure the network config matches.
441    async fn find_and_select_roam_candidate(
442        &self,
443        scan_type: fidl_common::ScanType,
444        network: types::NetworkIdentifier,
445        credential: &network_config::Credential,
446        current_security: types::SecurityTypeDetailed,
447    ) -> Option<types::ScannedCandidate> {
448        // Scan for APs in the provided network.
449        let mut matching_scan_results =
450            self.roam_scan(scan_type, network.clone(), current_security).await;
451        if matching_scan_results.is_empty() && scan_type == fidl_common::ScanType::Passive {
452            info!("No scan results seen in passive roam scan. Active scanning.");
453            matching_scan_results = self
454                .roam_scan(fidl_common::ScanType::Active, network.clone(), current_security)
455                .await;
456        }
457
458        let mut candidates = Vec::new();
459        // All APs should be grouped in the same scan result based on SSID and security,
460        // but combine all if there are multiple scan results.
461        for mut s in matching_scan_results {
462            if let Some(config) = self
463                .saved_network_manager
464                .lookup(&network)
465                .await
466                .into_iter()
467                .find(|c| &c.credential == credential)
468            {
469                candidates.append(&mut merge_config_and_scan_data(config, &mut s));
470            } else {
471                // This should only happen if the network config was just removed as the scan
472                // happened.
473                warn!("Failed to find config for network to roam from");
474            }
475        }
476        // Choose the best AP
477        bss_selection::select_bss(
478            candidates,
479            types::ConnectReason::ProactiveNetworkSwitch,
480            self.inspect_node_for_connection_selection.clone(),
481            self.telemetry_sender.clone(),
482        )
483        .await
484    }
485}
486
487impl types::ScannedCandidate {
488    pub fn recent_failure_count(&self) -> u64 {
489        self.saved_network_info
490            .recent_failures
491            .iter()
492            .filter(|failure| failure.bssid == self.bss.bssid)
493            .count()
494            .try_into()
495            .unwrap_or_else(|e| {
496                error!("{}", e);
497                u64::MAX
498            })
499    }
500    pub fn recent_short_connections(&self) -> usize {
501        self.saved_network_info
502            .past_connections
503            .get_list_for_bss(&self.bss.bssid)
504            .get_recent(fasync::MonotonicInstant::now() - RECENT_DISCONNECT_WINDOW)
505            .iter()
506            .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION)
507            .count()
508    }
509
510    pub fn saved_security_type_to_string(&self) -> String {
511        match self.network.security_type {
512            SecurityType::None => "open",
513            SecurityType::Wep => "WEP",
514            SecurityType::Wpa => "WPA1",
515            SecurityType::Wpa2 => "WPA2",
516            SecurityType::Wpa3 => "WPA3",
517        }
518        .to_string()
519    }
520
521    pub fn scanned_security_type_to_string(&self) -> String {
522        match self.security_type_detailed {
523            SecurityTypeDetailed::Unknown => "unknown",
524            SecurityTypeDetailed::Open => "open",
525            SecurityTypeDetailed::Wep => "WEP",
526            SecurityTypeDetailed::Wpa1 => "WPA1",
527            SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk",
528            SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk",
529            SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2",
530            SecurityTypeDetailed::Wpa2Personal => "WPA2",
531            SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3",
532            SecurityTypeDetailed::Wpa3Personal => "WPA3",
533            SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent",
534            SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent",
535        }
536        .to_string()
537    }
538
539    pub fn to_string_without_pii(&self) -> String {
540        let channel = self.bss.channel;
541        let rssi = self.bss.signal.rssi_dbm;
542        let recent_failure_count = self.recent_failure_count();
543        let recent_short_connection_count = self.recent_short_connections();
544
545        format!(
546            "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}",
547            self.network.ssid,
548            self.saved_security_type_to_string(),
549            self.bss.bssid,
550            self.scanned_security_type_to_string(),
551            rssi,
552            channel,
553            scoring_functions::score_bss_scanned_candidate(self.clone()),
554            if !self.bss.is_compatible() { ", NOT compatible" } else { "" },
555            if recent_failure_count > 0 {
556                format!(", {recent_failure_count} recent failures")
557            } else {
558                "".to_string()
559            },
560            if recent_short_connection_count > 0 {
561                format!(", {recent_short_connection_count} recent short disconnects")
562            } else {
563                "".to_string()
564            },
565            if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" },
566        )
567    }
568}
569
570impl WriteInspect for types::ScannedCandidate {
571    fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
572        inspect_insert!(writer, var key: {
573            ssid: self.network.ssid.to_string(),
574            bssid: self.bss.bssid.to_string(),
575            rssi: self.bss.signal.rssi_dbm,
576            score: scoring_functions::score_bss_scanned_candidate(self.clone()),
577            security_type_saved: self.saved_security_type_to_string(),
578            security_type_scanned: format!(
579                "{}",
580                wlan_common::bss::Protection::from(self.security_type_detailed),
581            ),
582            channel: format!("{}", self.bss.channel),
583            compatible: self.bss.is_compatible(),
584            incompatibility: self.bss
585                .compatibility
586                .as_ref()
587                .err()
588                .map(ToString::to_string)
589                .unwrap_or_else(|| String::from("none")),
590            recent_failure_count: self.recent_failure_count(),
591            saved_network_has_ever_connected: self.saved_network_info.has_ever_connected,
592        });
593    }
594}
595
596fn get_authenticator(bss: &Bss, credential: &Credential) -> Option<SecurityAuthenticator> {
597    let mutual_security_protocols = match bss.compatibility.as_ref() {
598        Ok(compatible) => compatible.mutual_security_protocols().clone(),
599        Err(incompatible) => {
600            error!("BSS ({:?}) is incompatible: {}", bss.bssid, incompatible);
601            return None;
602        }
603    };
604
605    match select_authentication_method(mutual_security_protocols.clone(), credential) {
606        Some(authenticator) => Some(authenticator),
607        None => {
608            error!(
609                "Failed to negotiate authentication for BSS ({:?}) with mutually supported \
610                 security protocols: {:?}, and credential type: {:?}.",
611                bss.bssid,
612                mutual_security_protocols,
613                credential.type_str()
614            );
615            None
616        }
617    }
618}
619
620fn merge_config_and_scan_data(
621    network_config: config_management::NetworkConfig,
622    scan_result: &mut types::ScanResult,
623) -> Vec<types::ScannedCandidate> {
624    if network_config.ssid != scan_result.ssid
625        || !network_config
626            .security_type
627            .is_compatible_with_scanned_type(&scan_result.security_type_detailed)
628    {
629        return Vec::new();
630    }
631
632    let mut merged_networks = Vec::new();
633    let multiple_bss_candidates = scan_result.entries.len() > 1;
634
635    for bss in scan_result.entries.iter() {
636        let authenticator = match get_authenticator(bss, &network_config.credential) {
637            Some(authenticator) => authenticator,
638            None => {
639                error!(
640                    "Failed to create authenticator for bss candidate {:?} (SSID: {:?}). Removing from candidates.",
641                    bss.bssid, &network_config.ssid
642                );
643                continue;
644            }
645        };
646        let scanned_candidate = types::ScannedCandidate {
647            network: types::NetworkIdentifier {
648                ssid: network_config.ssid.clone(),
649                security_type: network_config.security_type,
650            },
651            security_type_detailed: scan_result.security_type_detailed,
652            credential: network_config.credential.clone(),
653            network_has_multiple_bss: multiple_bss_candidates,
654            saved_network_info: InternalSavedNetworkData {
655                has_ever_connected: network_config.has_ever_connected,
656                recent_failures: network_config.perf_stats.connect_failures.get_recent_for_network(
657                    fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
658                ),
659                past_connections: network_config.perf_stats.past_connections.clone(),
660            },
661            bss: bss.clone(),
662            authenticator,
663        };
664        merged_networks.push(scanned_candidate)
665    }
666    merged_networks
667}
668
669/// Merge the saved networks and scan results into a vector of BSS candidates that correspond to a
670/// saved network.
671async fn merge_saved_networks_and_scan_data(
672    saved_network_manager: &Arc<dyn SavedNetworksManagerApi>,
673    mut scan_results: Vec<types::ScanResult>,
674) -> Vec<types::ScannedCandidate> {
675    let mut merged_networks = vec![];
676    for mut scan_result in scan_results.drain(..) {
677        for saved_config in saved_network_manager
678            .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed)
679            .await
680        {
681            let multiple_bss_candidates = scan_result.entries.len() > 1;
682            for bss in scan_result.entries.drain(..) {
683                let authenticator = match get_authenticator(&bss, &saved_config.credential) {
684                    Some(authenticator) => authenticator,
685                    None => {
686                        error!(
687                            "Failed to create authenticator for bss candidate {} (SSID: {}). Removing from candidates.",
688                            bss.bssid, saved_config.ssid
689                        );
690                        continue;
691                    }
692                };
693                let scanned_candidate = types::ScannedCandidate {
694                    network: types::NetworkIdentifier {
695                        ssid: saved_config.ssid.clone(),
696                        security_type: saved_config.security_type,
697                    },
698                    security_type_detailed: scan_result.security_type_detailed,
699                    credential: saved_config.credential.clone(),
700                    network_has_multiple_bss: multiple_bss_candidates,
701                    saved_network_info: InternalSavedNetworkData {
702                        has_ever_connected: saved_config.has_ever_connected,
703                        recent_failures: saved_config
704                            .perf_stats
705                            .connect_failures
706                            .get_recent_for_network(
707                                fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
708                            ),
709                        past_connections: saved_config.perf_stats.past_connections.clone(),
710                    },
711                    bss,
712                    authenticator,
713                };
714                merged_networks.push(scanned_candidate)
715            }
716        }
717    }
718    merged_networks
719}
720
721fn record_metrics_on_scan(
722    mut merged_networks: Vec<types::ScannedCandidate>,
723    telemetry_sender: &TelemetrySender,
724) {
725    let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<types::ScannedCandidate>> =
726        HashMap::new();
727    for bss in merged_networks.drain(..) {
728        merged_network_map.entry(bss.network.clone()).or_default().push(bss);
729    }
730
731    let num_saved_networks_observed = merged_network_map.len();
732    let mut num_actively_scanned_networks = 0;
733    let bss_count_per_saved_network = merged_network_map
734        .values()
735        .map(|bsss| {
736            // Check if the network was found via active scan.
737            if bsss.iter().any(|bss| matches!(bss.bss.observation, types::ScanObservation::Active))
738            {
739                num_actively_scanned_networks += 1;
740            };
741            // Count how many BSSs are visible in the scan results for this saved network.
742            bsss.len()
743        })
744        .collect();
745
746    telemetry_sender.send(TelemetryEvent::ConnectionSelectionScanResults {
747        saved_network_count: num_saved_networks_observed,
748        bss_count_per_saved_network,
749        saved_network_count_found_by_active_scan: num_actively_scanned_networks,
750    });
751}
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use crate::config_management::network_config::HistoricalListsByBssid;
756    use crate::config_management::{ConnectFailure, FailureReason, SavedNetworksManager};
757    use crate::util::testing::fakes::{FakeSavedNetworksManager, FakeScanRequester};
758    use crate::util::testing::{
759        create_inspect_persistence_channel, generate_channel, generate_random_bss,
760        generate_random_connect_reason, generate_random_network_identifier,
761        generate_random_password, generate_random_scan_result, generate_random_scanned_candidate,
762    };
763    use assert_matches::assert_matches;
764    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
765    use futures::task::Poll;
766    use ieee80211_testutils::BSSID_REGEX;
767    use rand::Rng;
768    use std::pin::pin;
769    use std::rc::Rc;
770    use std::sync::LazyLock;
771    use test_case::test_case;
772    use wlan_common::bss::BssDescription;
773    use wlan_common::random_fidl_bss_description;
774    use wlan_common::scan::Compatible;
775    use wlan_common::security::SecurityDescriptor;
776    use {
777        fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme,
778        fuchsia_async as fasync, fuchsia_inspect as inspect,
779    };
780
781    pub static TEST_PASSWORD: LazyLock<Credential> =
782        LazyLock::new(|| Credential::Password(b"password".to_vec()));
783
784    struct TestValues {
785        connection_selector: Rc<ConnectionSelector>,
786        real_saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
787        saved_network_manager: Arc<FakeSavedNetworksManager>,
788        scan_requester: Arc<FakeScanRequester>,
789        inspector: inspect::Inspector,
790        telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
791    }
792
793    async fn test_setup(use_real_save_network_manager: bool) -> TestValues {
794        let real_saved_network_manager = Arc::new(SavedNetworksManager::new_for_test().await);
795        let saved_network_manager = Arc::new(FakeSavedNetworksManager::new());
796        let scan_requester = Arc::new(FakeScanRequester::new());
797        let inspector = inspect::Inspector::default();
798        let inspect_node = inspector.root().create_child("connection_selection_test");
799        let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel();
800        let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
801
802        let connection_selector = Rc::new(ConnectionSelector::new(
803            if use_real_save_network_manager {
804                real_saved_network_manager.clone()
805            } else {
806                saved_network_manager.clone()
807            },
808            scan_requester.clone(),
809            inspect_node,
810            persistence_req_sender,
811            TelemetrySender::new(telemetry_sender),
812        ));
813
814        TestValues {
815            connection_selector,
816            real_saved_network_manager,
817            saved_network_manager,
818            scan_requester,
819            inspector,
820            telemetry_receiver,
821        }
822    }
823
824    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
825        fidl_sme::ConnectResult {
826            code: fidl_ieee80211::StatusCode::Success,
827            is_credential_rejected: false,
828            is_reconnect: false,
829        }
830    }
831
832    #[fuchsia::test]
833    async fn scan_results_merged_with_saved_networks() {
834        let test_values = test_setup(true).await;
835
836        // create some identifiers
837        let test_ssid_1 = types::Ssid::try_from("foo").unwrap();
838        let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal;
839        let test_id_1 = types::NetworkIdentifier {
840            ssid: test_ssid_1.clone(),
841            security_type: types::SecurityType::Wpa3,
842        };
843        let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
844        let test_ssid_2 = types::Ssid::try_from("bar").unwrap();
845        let test_security_2 = types::SecurityTypeDetailed::Wpa1;
846        let test_id_2 = types::NetworkIdentifier {
847            ssid: test_ssid_2.clone(),
848            security_type: types::SecurityType::Wpa,
849        };
850        let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
851
852        // insert the saved networks
853        assert!(
854            test_values
855                .real_saved_network_manager
856                .store(test_id_1.clone(), credential_1.clone())
857                .await
858                .unwrap()
859                .is_none()
860        );
861
862        assert!(
863            test_values
864                .real_saved_network_manager
865                .store(test_id_2.clone(), credential_2.clone())
866                .await
867                .unwrap()
868                .is_none()
869        );
870
871        // build some scan results
872        let mock_scan_results = vec![
873            types::ScanResult {
874                ssid: test_ssid_1.clone(),
875                security_type_detailed: test_security_1,
876                entries: vec![
877                    types::Bss {
878                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
879                        ..generate_random_bss()
880                    },
881                    types::Bss {
882                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
883                        ..generate_random_bss()
884                    },
885                    types::Bss {
886                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
887                        ..generate_random_bss()
888                    },
889                ],
890                compatibility: types::Compatibility::Supported,
891            },
892            types::ScanResult {
893                ssid: test_ssid_2.clone(),
894                security_type_detailed: test_security_2,
895                entries: vec![types::Bss {
896                    compatibility: Compatible::expect_ok([SecurityDescriptor::WPA1]),
897                    ..generate_random_bss()
898                }],
899                compatibility: types::Compatibility::DisallowedNotSupported,
900            },
901        ];
902
903        let bssid_1 = mock_scan_results[0].entries[0].bssid;
904        let bssid_2 = mock_scan_results[0].entries[1].bssid;
905
906        // mark the first one as having connected
907        test_values
908            .real_saved_network_manager
909            .record_connect_result(
910                test_id_1.clone(),
911                &credential_1.clone(),
912                bssid_1,
913                fake_successful_connect_result(),
914                types::ScanObservation::Unknown,
915            )
916            .await;
917
918        // mark the second one as having a failure
919        test_values
920            .real_saved_network_manager
921            .record_connect_result(
922                test_id_1.clone(),
923                &credential_1.clone(),
924                bssid_2,
925                fidl_sme::ConnectResult {
926                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
927                    is_credential_rejected: true,
928                    ..fake_successful_connect_result()
929                },
930                types::ScanObservation::Unknown,
931            )
932            .await;
933
934        // build our expected result
935        let failure_time = test_values
936            .real_saved_network_manager
937            .lookup(&test_id_1.clone())
938            .await
939            .first()
940            .expect("failed to get config")
941            .perf_stats
942            .connect_failures
943            .get_recent_for_network(fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW)
944            .first()
945            .expect("failed to get recent failure")
946            .time;
947        let recent_failures = vec![ConnectFailure {
948            bssid: bssid_2,
949            time: failure_time,
950            reason: FailureReason::CredentialRejected,
951        }];
952        let expected_internal_data_1 = InternalSavedNetworkData {
953            has_ever_connected: true,
954            recent_failures: recent_failures.clone(),
955            past_connections: HistoricalListsByBssid::new(),
956        };
957        let wpa3_authenticator = select_authentication_method(
958            HashSet::from([SecurityDescriptor::WPA3_PERSONAL]),
959            &credential_1,
960        )
961        .unwrap();
962        let open_authenticator =
963            select_authentication_method(HashSet::from([SecurityDescriptor::WPA1]), &credential_2)
964                .unwrap();
965        let expected_results = vec![
966            types::ScannedCandidate {
967                network: test_id_1.clone(),
968                credential: credential_1.clone(),
969                network_has_multiple_bss: true,
970                security_type_detailed: test_security_1,
971                saved_network_info: expected_internal_data_1.clone(),
972                bss: mock_scan_results[0].entries[0].clone(),
973                authenticator: wpa3_authenticator.clone(),
974            },
975            types::ScannedCandidate {
976                network: test_id_1.clone(),
977                credential: credential_1.clone(),
978                network_has_multiple_bss: true,
979                security_type_detailed: test_security_1,
980                saved_network_info: expected_internal_data_1.clone(),
981                bss: mock_scan_results[0].entries[1].clone(),
982                authenticator: wpa3_authenticator.clone(),
983            },
984            types::ScannedCandidate {
985                network: test_id_1.clone(),
986                credential: credential_1.clone(),
987                network_has_multiple_bss: true,
988                security_type_detailed: test_security_1,
989                saved_network_info: expected_internal_data_1.clone(),
990                bss: mock_scan_results[0].entries[2].clone(),
991                authenticator: wpa3_authenticator.clone(),
992            },
993            types::ScannedCandidate {
994                network: test_id_2.clone(),
995                credential: credential_2.clone(),
996                network_has_multiple_bss: false,
997                security_type_detailed: test_security_2,
998                saved_network_info: InternalSavedNetworkData {
999                    has_ever_connected: false,
1000                    recent_failures: Vec::new(),
1001                    past_connections: HistoricalListsByBssid::new(),
1002                },
1003                bss: mock_scan_results[1].entries[0].clone(),
1004                authenticator: open_authenticator.clone(),
1005            },
1006        ];
1007
1008        // validate the function works
1009        let results = merge_saved_networks_and_scan_data(
1010            // We're not yet ready to change all our Arc to Rc
1011            #[allow(clippy::arc_with_non_send_sync)]
1012            &Arc::new(test_values.real_saved_network_manager),
1013            mock_scan_results,
1014        )
1015        .await;
1016
1017        assert_eq!(results, expected_results);
1018    }
1019
1020    #[fuchsia::test]
1021    fn augment_bss_candidate_with_active_scan_doesnt_run_on_actively_found_networks() {
1022        let mut exec = fasync::TestExecutor::new();
1023        let test_values = exec.run_singlethreaded(test_setup(true));
1024        let mut candidate = generate_random_scanned_candidate();
1025        candidate.bss.observation = types::ScanObservation::Active;
1026
1027        let fut = test_values
1028            .connection_selector
1029            .augment_bss_candidate_with_active_scan(candidate.clone());
1030        let mut fut = pin!(fut);
1031
1032        // The connect_req comes out the other side with no change
1033        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(res) => {
1034            assert_eq!(&res, &candidate);
1035        });
1036    }
1037
1038    #[fuchsia::test]
1039    fn augment_bss_candidate_with_active_scan_runs_on_passively_found_networks() {
1040        let mut exec = fasync::TestExecutor::new();
1041        let test_values = exec.run_singlethreaded(test_setup(true));
1042
1043        let mut passively_scanned_candidate = generate_random_scanned_candidate();
1044        passively_scanned_candidate.bss.observation = types::ScanObservation::Passive;
1045
1046        let fut = test_values
1047            .connection_selector
1048            .augment_bss_candidate_with_active_scan(passively_scanned_candidate.clone());
1049        let fut = pin!(fut);
1050
1051        // Set the scan results
1052        let new_bss_desc = random_fidl_bss_description!();
1053        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1054            types::ScanResult {
1055                ssid: passively_scanned_candidate.network.ssid.clone(),
1056                security_type_detailed: passively_scanned_candidate.security_type_detailed,
1057                compatibility: types::Compatibility::Supported,
1058                entries: vec![types::Bss {
1059                    bssid: passively_scanned_candidate.bss.bssid,
1060                    compatibility: wlan_common::scan::Compatible::expect_ok([
1061                        wlan_common::security::SecurityDescriptor::WPA1,
1062                    ]),
1063                    bss_description: new_bss_desc.clone().into(),
1064                    ..generate_random_bss()
1065                }],
1066            },
1067        ])));
1068
1069        let candidate = exec.run_singlethreaded(fut);
1070        // The connect_req comes out the other side with the new bss_description
1071        assert_eq!(
1072            &candidate,
1073            &types::ScannedCandidate {
1074                bss: types::Bss {
1075                    bss_description: new_bss_desc.into(),
1076                    ..passively_scanned_candidate.bss.clone()
1077                },
1078                ..passively_scanned_candidate.clone()
1079            },
1080        );
1081
1082        // Check the right scan request was sent
1083        assert_eq!(
1084            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1085            vec![(
1086                ScanReason::BssSelectionAugmentation,
1087                vec![passively_scanned_candidate.network.ssid.clone()],
1088                vec![passively_scanned_candidate.bss.channel]
1089            )]
1090        );
1091    }
1092
1093    #[fuchsia::test]
1094    fn find_available_bss_list_with_network_specified() {
1095        let mut exec = fasync::TestExecutor::new();
1096        let test_values = exec.run_singlethreaded(test_setup(false));
1097        let connection_selector = test_values.connection_selector;
1098
1099        // create identifiers
1100        let test_id_1 = types::NetworkIdentifier {
1101            ssid: types::Ssid::try_from("foo").unwrap(),
1102            security_type: types::SecurityType::Wpa3,
1103        };
1104        let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1105
1106        // insert saved networks
1107        assert!(
1108            exec.run_singlethreaded(
1109                test_values.saved_network_manager.store(test_id_1.clone(), credential_1.clone()),
1110            )
1111            .unwrap()
1112            .is_none()
1113        );
1114
1115        // Prep the scan results
1116        let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1117        let bss_desc_1 = random_fidl_bss_description!();
1118        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1119            types::ScanResult {
1120                ssid: test_id_1.ssid.clone(),
1121                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1122                compatibility: types::Compatibility::Supported,
1123                entries: vec![types::Bss {
1124                    compatibility: wlan_common::scan::Compatible::expect_ok(
1125                        mutual_security_protocols_1,
1126                    ),
1127                    bss_description: bss_desc_1.clone().into(),
1128                    ..generate_random_bss()
1129                }],
1130            },
1131            generate_random_scan_result(),
1132            generate_random_scan_result(),
1133        ])));
1134
1135        // Run the scan, specifying the desired network
1136        let fut = connection_selector.find_available_bss_candidate_list(Some(test_id_1.clone()));
1137        let mut fut = pin!(fut);
1138        let results = exec.run_singlethreaded(&mut fut);
1139        assert_eq!(results.len(), 1);
1140
1141        // Check that the right scan request was sent
1142        assert_eq!(
1143            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1144            vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1145        );
1146    }
1147
1148    #[test_case(true)]
1149    #[test_case(false)]
1150    #[fuchsia::test(add_test_attr = false)]
1151    fn find_available_bss_list_without_network_specified(hidden: bool) {
1152        let mut exec = fasync::TestExecutor::new();
1153        let mut test_values = exec.run_singlethreaded(test_setup(false));
1154        let connection_selector = test_values.connection_selector;
1155
1156        // create identifiers
1157        let test_id_not_hidden = types::NetworkIdentifier {
1158            ssid: types::Ssid::try_from("foo").unwrap(),
1159            security_type: types::SecurityType::Wpa3,
1160        };
1161        let test_id_maybe_hidden = types::NetworkIdentifier {
1162            ssid: types::Ssid::try_from("bar").unwrap(),
1163            security_type: types::SecurityType::Wpa3,
1164        };
1165        let test_id_hidden_but_seen = types::NetworkIdentifier {
1166            ssid: types::Ssid::try_from("baz").unwrap(),
1167            security_type: types::SecurityType::Wpa3,
1168        };
1169        let credential = Credential::Password("some_pass".as_bytes().to_vec());
1170
1171        // insert saved networks
1172        assert!(
1173            exec.run_singlethreaded(
1174                test_values
1175                    .saved_network_manager
1176                    .store(test_id_not_hidden.clone(), credential.clone()),
1177            )
1178            .unwrap()
1179            .is_none()
1180        );
1181        assert!(
1182            exec.run_singlethreaded(
1183                test_values
1184                    .saved_network_manager
1185                    .store(test_id_maybe_hidden.clone(), credential.clone()),
1186            )
1187            .unwrap()
1188            .is_none()
1189        );
1190        assert!(
1191            exec.run_singlethreaded(
1192                test_values
1193                    .saved_network_manager
1194                    .store(test_id_hidden_but_seen.clone(), credential.clone()),
1195            )
1196            .unwrap()
1197            .is_none()
1198        );
1199
1200        // Set the hidden probability for test_id_not_hidden and test_id_hidden_but_seen
1201        exec.run_singlethreaded(
1202            test_values.saved_network_manager.update_hidden_prob(test_id_not_hidden.clone(), 0.0),
1203        );
1204        exec.run_singlethreaded(
1205            test_values
1206                .saved_network_manager
1207                .update_hidden_prob(test_id_hidden_but_seen.clone(), 1.0),
1208        );
1209        // Set the hidden probability for test_id_maybe_hidden based on test variant
1210        exec.run_singlethreaded(
1211            test_values
1212                .saved_network_manager
1213                .update_hidden_prob(test_id_maybe_hidden.clone(), if hidden { 1.0 } else { 0.0 }),
1214        );
1215
1216        // Prep the scan results
1217        let mutual_security_protocols = [SecurityDescriptor::WPA3_PERSONAL];
1218        let bss_desc = random_fidl_bss_description!();
1219        if hidden {
1220            // Initial passive scan has non-hidden result and hidden-but-seen result
1221            exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1222                types::ScanResult {
1223                    ssid: test_id_not_hidden.ssid.clone(),
1224                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1225                    compatibility: types::Compatibility::Supported,
1226                    entries: vec![types::Bss {
1227                        compatibility: wlan_common::scan::Compatible::expect_ok(
1228                            mutual_security_protocols,
1229                        ),
1230                        bss_description: bss_desc.clone().into(),
1231                        ..generate_random_bss()
1232                    }],
1233                },
1234                types::ScanResult {
1235                    ssid: test_id_hidden_but_seen.ssid.clone(),
1236                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1237                    compatibility: types::Compatibility::Supported,
1238                    entries: vec![types::Bss {
1239                        compatibility: wlan_common::scan::Compatible::expect_ok(
1240                            mutual_security_protocols,
1241                        ),
1242                        bss_description: bss_desc.clone().into(),
1243                        ..generate_random_bss()
1244                    }],
1245                },
1246                generate_random_scan_result(),
1247                generate_random_scan_result(),
1248            ])));
1249            // Next active scan has hidden result
1250            exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1251                types::ScanResult {
1252                    ssid: test_id_maybe_hidden.ssid.clone(),
1253                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1254                    compatibility: types::Compatibility::Supported,
1255                    entries: vec![types::Bss {
1256                        compatibility: wlan_common::scan::Compatible::expect_ok(
1257                            mutual_security_protocols,
1258                        ),
1259                        bss_description: bss_desc.clone().into(),
1260                        ..generate_random_bss()
1261                    }],
1262                },
1263                generate_random_scan_result(),
1264                generate_random_scan_result(),
1265            ])));
1266        } else {
1267            // Non-hidden test case, only one scan, return all results
1268            exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1269                types::ScanResult {
1270                    ssid: test_id_not_hidden.ssid.clone(),
1271                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1272                    compatibility: types::Compatibility::Supported,
1273                    entries: vec![types::Bss {
1274                        compatibility: wlan_common::scan::Compatible::expect_ok(
1275                            mutual_security_protocols,
1276                        ),
1277                        bss_description: bss_desc.clone().into(),
1278                        ..generate_random_bss()
1279                    }],
1280                },
1281                types::ScanResult {
1282                    ssid: test_id_maybe_hidden.ssid.clone(),
1283                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1284                    compatibility: types::Compatibility::Supported,
1285                    entries: vec![types::Bss {
1286                        compatibility: wlan_common::scan::Compatible::expect_ok(
1287                            mutual_security_protocols,
1288                        ),
1289                        bss_description: bss_desc.clone().into(),
1290                        ..generate_random_bss()
1291                    }],
1292                },
1293                types::ScanResult {
1294                    ssid: test_id_hidden_but_seen.ssid.clone(),
1295                    security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1296                    compatibility: types::Compatibility::Supported,
1297                    entries: vec![types::Bss {
1298                        compatibility: wlan_common::scan::Compatible::expect_ok(
1299                            mutual_security_protocols,
1300                        ),
1301                        bss_description: bss_desc.clone().into(),
1302                        ..generate_random_bss()
1303                    }],
1304                },
1305                generate_random_scan_result(),
1306                generate_random_scan_result(),
1307            ])));
1308        }
1309
1310        // Run the scan(s)
1311        let connection_selection_fut = connection_selector.find_available_bss_candidate_list(None);
1312        let mut connection_selection_fut = pin!(connection_selection_fut);
1313        let results = exec.run_singlethreaded(&mut connection_selection_fut);
1314        assert_eq!(results.len(), 3);
1315
1316        // Check that the right scan request(s) were sent. The hidden-but-seen SSID should never
1317        // be requested for the active scan.
1318        if hidden {
1319            assert_eq!(
1320                *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1321                vec![
1322                    (ScanReason::NetworkSelection, vec![], vec![]),
1323                    (ScanReason::NetworkSelection, vec![test_id_maybe_hidden.ssid.clone()], vec![])
1324                ]
1325            )
1326        } else {
1327            assert_eq!(
1328                *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1329                vec![(ScanReason::NetworkSelection, vec![], vec![])]
1330            )
1331        }
1332
1333        // Check that the metrics were logged
1334        assert_matches!(
1335            test_values.telemetry_receiver.try_next(),
1336            Ok(Some(TelemetryEvent::ActiveScanRequested{num_ssids_requested})) => {
1337                if hidden {
1338                        assert_eq!(num_ssids_requested, 1);
1339                } else {
1340                        assert_eq!(num_ssids_requested, 0);
1341                }
1342        });
1343    }
1344
1345    #[fuchsia::test]
1346    fn find_and_select_connection_candidate_scan_error() {
1347        let mut exec = fasync::TestExecutor::new();
1348        let test_values = exec.run_singlethreaded(test_setup(true));
1349        let connection_selector = test_values.connection_selector;
1350        let mut telemetry_receiver = test_values.telemetry_receiver;
1351
1352        // Return an error on the scan
1353        exec.run_singlethreaded(
1354            test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1355        );
1356
1357        // Kick off network selection
1358        let mut connection_selection_fut = pin!(
1359            connection_selector
1360                .find_and_select_connection_candidate(None, generate_random_connect_reason())
1361        );
1362        // Check that nothing is returned
1363        assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(None));
1364
1365        // Check that the right scan request was sent
1366        assert_eq!(
1367            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1368            vec![(scan::ScanReason::NetworkSelection, vec![], vec![])]
1369        );
1370
1371        // Check the network selections were logged
1372        assert_data_tree!(@executor exec, test_values.inspector, root: {
1373            connection_selection_test: {
1374                connection_selection: {
1375                    "0": {
1376                        "@time": AnyNumericProperty,
1377                        "candidates": {},
1378                    },
1379                }
1380            },
1381        });
1382
1383        // Verify TelemetryEvent for network selection was sent
1384        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1385            assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1386                network_selection_type: telemetry::NetworkSelectionType::Undirected,
1387                num_candidates: Err(()),
1388                selected_count: 0,
1389            });
1390        });
1391        assert_matches!(
1392            telemetry_receiver.try_next(),
1393            Ok(Some(TelemetryEvent::BssSelectionResult { selected_candidate: None, .. }))
1394        );
1395    }
1396
1397    #[fuchsia::test]
1398    fn find_and_select_connection_candidate_end_to_end() {
1399        let mut exec = fasync::TestExecutor::new();
1400        let test_values = exec.run_singlethreaded(test_setup(true));
1401        let connection_selector = test_values.connection_selector;
1402        let mut telemetry_receiver = test_values.telemetry_receiver;
1403
1404        // create some identifiers
1405        let test_id_1 = types::NetworkIdentifier {
1406            ssid: types::Ssid::try_from("foo").unwrap(),
1407            security_type: types::SecurityType::Wpa3,
1408        };
1409        let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
1410        let bssid_1 = types::Bssid::from([1, 1, 1, 1, 1, 1]);
1411
1412        let test_id_2 = types::NetworkIdentifier {
1413            ssid: types::Ssid::try_from("bar").unwrap(),
1414            security_type: types::SecurityType::Wpa,
1415        };
1416        let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
1417        let bssid_2 = types::Bssid::from([2, 2, 2, 2, 2, 2]);
1418
1419        // insert some new saved networks
1420        assert!(
1421            exec.run_singlethreaded(
1422                test_values
1423                    .real_saved_network_manager
1424                    .store(test_id_1.clone(), credential_1.clone()),
1425            )
1426            .unwrap()
1427            .is_none()
1428        );
1429        assert!(
1430            exec.run_singlethreaded(
1431                test_values
1432                    .real_saved_network_manager
1433                    .store(test_id_2.clone(), credential_2.clone()),
1434            )
1435            .unwrap()
1436            .is_none()
1437        );
1438
1439        // Mark them as having connected. Make connection passive so that no active scans are made.
1440        exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1441            test_id_1.clone(),
1442            &credential_1.clone(),
1443            bssid_1,
1444            fake_successful_connect_result(),
1445            types::ScanObservation::Passive,
1446        ));
1447        exec.run_singlethreaded(test_values.real_saved_network_manager.record_connect_result(
1448            test_id_2.clone(),
1449            &credential_2.clone(),
1450            bssid_2,
1451            fake_successful_connect_result(),
1452            types::ScanObservation::Passive,
1453        ));
1454
1455        // Prep the scan results
1456        let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1457        let channel_1 = generate_channel(3);
1458        let mutual_security_protocols_2 = [SecurityDescriptor::WPA1];
1459        let channel_2 = generate_channel(50);
1460        let mock_passive_scan_results = vec![
1461            types::ScanResult {
1462                ssid: test_id_1.ssid.clone(),
1463                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1464                compatibility: types::Compatibility::Supported,
1465                entries: vec![types::Bss {
1466                    compatibility: wlan_common::scan::Compatible::expect_ok(
1467                        mutual_security_protocols_1,
1468                    ),
1469                    bssid: bssid_1,
1470                    channel: channel_1,
1471                    signal: types::Signal {
1472                        rssi_dbm: -30, // much higher than other result
1473                        snr_db: 0,
1474                    },
1475                    observation: types::ScanObservation::Passive,
1476                    ..generate_random_bss()
1477                }],
1478            },
1479            types::ScanResult {
1480                ssid: test_id_2.ssid.clone(),
1481                security_type_detailed: types::SecurityTypeDetailed::Wpa1,
1482                compatibility: types::Compatibility::Supported,
1483                entries: vec![types::Bss {
1484                    compatibility: wlan_common::scan::Compatible::expect_ok(
1485                        mutual_security_protocols_2,
1486                    ),
1487                    bssid: bssid_2,
1488                    channel: channel_2,
1489                    signal: types::Signal {
1490                        rssi_dbm: -100, // much lower than other result
1491                        snr_db: 0,
1492                    },
1493                    observation: types::ScanObservation::Passive,
1494                    ..generate_random_bss()
1495                }],
1496            },
1497            generate_random_scan_result(),
1498            generate_random_scan_result(),
1499        ];
1500
1501        // Initial passive scan
1502        exec.run_singlethreaded(
1503            test_values.scan_requester.add_scan_result(Ok(mock_passive_scan_results.clone())),
1504        );
1505
1506        // An additional directed active scan should be made for the selected network
1507        let bss_desc1_active = random_fidl_bss_description!();
1508        let new_bss = types::Bss {
1509            compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1510            bssid: bssid_1,
1511            bss_description: bss_desc1_active.clone().into(),
1512            ..generate_random_bss()
1513        };
1514        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1515            types::ScanResult {
1516                ssid: test_id_1.ssid.clone(),
1517                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1518                compatibility: types::Compatibility::Supported,
1519                entries: vec![new_bss.clone()],
1520            },
1521            generate_random_scan_result(),
1522        ])));
1523
1524        // Check that we pick a network
1525        let mut connection_selection_fut = pin!(
1526            connection_selector
1527                .find_and_select_connection_candidate(None, generate_random_connect_reason())
1528        );
1529        let results =
1530            exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1531        assert_eq!(&results.network, &test_id_1.clone());
1532        assert_eq!(
1533            &results.bss,
1534            &types::Bss {
1535                bss_description: bss_desc1_active.clone().into(),
1536                ..mock_passive_scan_results[0].entries[0].clone()
1537            }
1538        );
1539
1540        // Check that the right scan requests were sent
1541        assert_eq!(
1542            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1543            vec![
1544                // Initial passive scan
1545                (ScanReason::NetworkSelection, vec![], vec![]),
1546                // Directed active scan should be made for the selected network
1547                (
1548                    ScanReason::BssSelectionAugmentation,
1549                    vec![test_id_1.ssid.clone()],
1550                    vec![channel_1]
1551                )
1552            ]
1553        );
1554
1555        // Check the network selections were logged
1556        assert_data_tree!(@executor exec, test_values.inspector, root: {
1557            connection_selection_test: {
1558                connection_selection: {
1559                    "0": {
1560                        "@time": AnyNumericProperty,
1561                        "candidates": {
1562                            "0": contains {
1563                                bssid: &*BSSID_REGEX,
1564                                score: AnyNumericProperty,
1565                            },
1566                            "1": contains {
1567                                bssid: &*BSSID_REGEX,
1568                                score: AnyNumericProperty,
1569                            },
1570                        },
1571                        "selected": contains {
1572                            bssid: &*BSSID_REGEX,
1573                            score: AnyNumericProperty,
1574                        },
1575                    },
1576                }
1577            },
1578        });
1579
1580        // Verify TelemetryEvents for network selection were sent
1581        assert_matches!(
1582            telemetry_receiver.try_next(),
1583            Ok(Some(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 0 }))
1584        );
1585        assert_matches!(
1586            telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1587                saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1588            })) => {
1589                assert_eq!(saved_network_count, 2);
1590                assert_eq!(bss_count_per_saved_network, vec![1, 1]);
1591                assert_eq!(saved_network_count_found_by_active_scan, 0);
1592            }
1593        );
1594        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1595            assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1596                network_selection_type: telemetry::NetworkSelectionType::Undirected,
1597                num_candidates: Ok(2),
1598                selected_count: 2,
1599            });
1600        });
1601        assert_matches!(
1602            telemetry_receiver.try_next(),
1603            Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1604        );
1605    }
1606
1607    #[fuchsia::test]
1608    fn find_and_select_connection_candidate_with_network_end_to_end() {
1609        let mut exec = fasync::TestExecutor::new();
1610        let test_values = exec.run_singlethreaded(test_setup(true));
1611        let connection_selector = test_values.connection_selector;
1612        let mut telemetry_receiver = test_values.telemetry_receiver;
1613
1614        // create identifiers
1615        let test_id_1 = types::NetworkIdentifier {
1616            ssid: types::Ssid::try_from("foo").unwrap(),
1617            security_type: types::SecurityType::Wpa3,
1618        };
1619
1620        // insert saved networks
1621        assert!(
1622            exec.run_singlethreaded(
1623                test_values
1624                    .real_saved_network_manager
1625                    .store(test_id_1.clone(), TEST_PASSWORD.clone()),
1626            )
1627            .unwrap()
1628            .is_none()
1629        );
1630
1631        // Prep the scan results
1632        let mutual_security_protocols_1 = [SecurityDescriptor::WPA3_PERSONAL];
1633        let bss_desc_1 = random_fidl_bss_description!();
1634        let scanned_bss = types::Bss {
1635            // This network is WPA3, but should still match against the desired WPA2 network
1636            compatibility: wlan_common::scan::Compatible::expect_ok(mutual_security_protocols_1),
1637            bss_description: bss_desc_1.clone().into(),
1638            // Observation should be unknown, since we didn't try a passive scan.
1639            observation: types::ScanObservation::Unknown,
1640            ..generate_random_bss()
1641        };
1642        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1643            types::ScanResult {
1644                ssid: test_id_1.ssid.clone(),
1645                // This network is WPA3, but should still match against the desired WPA2 network
1646                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1647                compatibility: types::Compatibility::Supported,
1648                entries: vec![scanned_bss.clone()],
1649            },
1650            generate_random_scan_result(),
1651            generate_random_scan_result(),
1652        ])));
1653
1654        // Run network selection
1655        let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1656            Some(test_id_1.clone()),
1657            generate_random_connect_reason(),
1658        );
1659        let mut connection_selection_fut = pin!(connection_selection_fut);
1660        let results =
1661            exec.run_singlethreaded(&mut connection_selection_fut).expect("no selected candidate");
1662        assert_eq!(&results.network, &test_id_1);
1663        assert_eq!(&results.security_type_detailed, &types::SecurityTypeDetailed::Wpa3Personal);
1664        assert_eq!(&results.credential, &TEST_PASSWORD.clone());
1665        assert_eq!(&results.bss, &scanned_bss);
1666
1667        // Check that the right scan request was sent
1668        assert_eq!(
1669            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1670            vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1671        );
1672        // Verify that NetworkSelectionDecision telemetry event is sent
1673        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1674            assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1675                network_selection_type: telemetry::NetworkSelectionType::Directed,
1676                num_candidates: Ok(1),
1677                selected_count: 1,
1678            });
1679        });
1680        assert_matches!(
1681            telemetry_receiver.try_next(),
1682            Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1683        );
1684    }
1685
1686    #[fuchsia::test]
1687    fn find_and_select_connection_candidate_with_network_end_to_end_with_failure() {
1688        let mut exec = fasync::TestExecutor::new();
1689        let test_values = exec.run_singlethreaded(test_setup(true));
1690        let connection_selector = test_values.connection_selector;
1691        let mut telemetry_receiver = test_values.telemetry_receiver;
1692
1693        // create identifiers
1694        let test_id_1 = types::NetworkIdentifier {
1695            ssid: types::Ssid::try_from("foo").unwrap(),
1696            security_type: types::SecurityType::Wpa3,
1697        };
1698
1699        // Return an error on the scan
1700        exec.run_singlethreaded(
1701            test_values.scan_requester.add_scan_result(Err(types::ScanError::GeneralError)),
1702        );
1703
1704        // Kick off network selection
1705        let connection_selection_fut = connection_selector.find_and_select_connection_candidate(
1706            Some(test_id_1.clone()),
1707            generate_random_connect_reason(),
1708        );
1709        let mut connection_selection_fut = pin!(connection_selection_fut);
1710
1711        // Check that nothing is returned
1712        let results = exec.run_singlethreaded(&mut connection_selection_fut);
1713        assert_eq!(results, None);
1714
1715        // Check that the right scan request was sent
1716        assert_eq!(
1717            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1718            vec![(ScanReason::BssSelection, vec![test_id_1.ssid.clone()], vec![])]
1719        );
1720
1721        // Verify that NetworkSelectionDecision telemetry event is sent
1722        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
1723            assert_matches!(event, TelemetryEvent::NetworkSelectionDecision {
1724                network_selection_type: telemetry::NetworkSelectionType::Directed,
1725                num_candidates: Err(()),
1726                selected_count: 1,
1727            });
1728        });
1729
1730        assert_matches!(
1731            telemetry_receiver.try_next(),
1732            Ok(Some(TelemetryEvent::BssSelectionResult { .. }))
1733        );
1734    }
1735
1736    #[test_case(fidl_common::ScanType::Active)]
1737    #[test_case(fidl_common::ScanType::Passive)]
1738    #[fuchsia::test(add_test_attr = false)]
1739    fn find_and_select_roam_candidate_requests_typed_scan(scan_type: fidl_common::ScanType) {
1740        let mut exec = fasync::TestExecutor::new();
1741        let test_values = exec.run_singlethreaded(test_setup(true));
1742        let connection_selector = test_values.connection_selector;
1743
1744        let test_id = types::NetworkIdentifier {
1745            ssid: types::Ssid::try_from("foo").unwrap(),
1746            security_type: types::SecurityType::Wpa3,
1747        };
1748        let credential = generate_random_password();
1749
1750        // Set a scan result to be returned
1751        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![
1752            types::ScanResult {
1753                ssid: test_id.ssid.clone(),
1754                security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal,
1755                compatibility: types::Compatibility::Supported,
1756                entries: vec![],
1757            },
1758        ])));
1759
1760        // Kick off roam selection
1761        let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1762            scan_type,
1763            test_id.clone(),
1764            &credential,
1765            types::SecurityTypeDetailed::Wpa3Personal,
1766        );
1767        let mut roam_selection_fut = pin!(roam_selection_fut);
1768        let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1769
1770        // Check that the right scan request was sent
1771        if scan_type == fidl_common::ScanType::Active {
1772            assert_eq!(
1773                *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1774                vec![(ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])]
1775            );
1776        } else {
1777            assert_eq!(
1778                *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1779                vec![(ScanReason::RoamSearch, vec![], vec![])]
1780            );
1781        }
1782    }
1783
1784    #[fuchsia::test]
1785    fn find_and_select_roam_candidate_active_scans_if_no_results_found() {
1786        let mut exec = fasync::TestExecutor::new();
1787        let test_values = exec.run_singlethreaded(test_setup(true));
1788        let connection_selector = test_values.connection_selector;
1789
1790        let test_id = generate_random_network_identifier();
1791        let credential = generate_random_password();
1792
1793        // Set empty scan results to be returned.
1794        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1795        exec.run_singlethreaded(test_values.scan_requester.add_scan_result(Ok(vec![])));
1796
1797        // Kick off roam selection with a passive scan.
1798        let roam_selection_fut = connection_selector.find_and_select_roam_candidate(
1799            fidl_common::ScanType::Passive,
1800            test_id.clone(),
1801            &credential,
1802            types::SecurityTypeDetailed::Open,
1803        );
1804
1805        let mut roam_selection_fut = pin!(roam_selection_fut);
1806        let _ = exec.run_singlethreaded(&mut roam_selection_fut);
1807
1808        // Check that two scan requests were issued, one passive scan and one active scan when the
1809        // passive scan yielded no results.
1810        assert_eq!(
1811            *exec.run_singlethreaded(test_values.scan_requester.scan_requests.lock()),
1812            vec![
1813                (ScanReason::RoamSearch, vec![], vec![]),
1814                (ScanReason::RoamSearch, vec![test_id.ssid.clone()], vec![])
1815            ]
1816        );
1817    }
1818
1819    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
1820    #[fuchsia::test]
1821    async fn recorded_metrics_on_scan() {
1822        let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1823        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1824
1825        // create some identifiers
1826        let test_id_1 = types::NetworkIdentifier {
1827            ssid: types::Ssid::try_from("foo").unwrap().clone(),
1828            security_type: types::SecurityType::Wpa3,
1829        };
1830
1831        let test_id_2 = types::NetworkIdentifier {
1832            ssid: types::Ssid::try_from("bar").unwrap().clone(),
1833            security_type: types::SecurityType::Wpa,
1834        };
1835
1836        let mut mock_scan_results = vec![];
1837
1838        mock_scan_results.push(types::ScannedCandidate {
1839            network: test_id_1.clone(),
1840            bss: types::Bss {
1841                observation: types::ScanObservation::Passive,
1842                ..generate_random_bss()
1843            },
1844            ..generate_random_scanned_candidate()
1845        });
1846        mock_scan_results.push(types::ScannedCandidate {
1847            network: test_id_1.clone(),
1848            bss: types::Bss {
1849                observation: types::ScanObservation::Passive,
1850                ..generate_random_bss()
1851            },
1852            ..generate_random_scanned_candidate()
1853        });
1854        mock_scan_results.push(types::ScannedCandidate {
1855            network: test_id_1.clone(),
1856            bss: types::Bss {
1857                observation: types::ScanObservation::Active,
1858                ..generate_random_bss()
1859            },
1860            ..generate_random_scanned_candidate()
1861        });
1862        mock_scan_results.push(types::ScannedCandidate {
1863            network: test_id_2.clone(),
1864            bss: types::Bss {
1865                observation: types::ScanObservation::Passive,
1866                ..generate_random_bss()
1867            },
1868            ..generate_random_scanned_candidate()
1869        });
1870
1871        record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1872
1873        assert_matches!(
1874            telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1875                saved_network_count, mut bss_count_per_saved_network, saved_network_count_found_by_active_scan
1876            })) => {
1877                assert_eq!(saved_network_count, 2);
1878                bss_count_per_saved_network.sort();
1879                assert_eq!(bss_count_per_saved_network, vec![1, 3]);
1880                assert_eq!(saved_network_count_found_by_active_scan, 1);
1881            }
1882        );
1883    }
1884
1885    #[fuchsia::test]
1886    async fn recorded_metrics_on_scan_no_saved_networks() {
1887        let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1888        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1889
1890        let mock_scan_results = vec![];
1891
1892        record_metrics_on_scan(mock_scan_results, &telemetry_sender);
1893
1894        assert_matches!(
1895            telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::ConnectionSelectionScanResults {
1896                saved_network_count, bss_count_per_saved_network, saved_network_count_found_by_active_scan
1897            })) => {
1898                assert_eq!(saved_network_count, 0);
1899                assert_eq!(bss_count_per_saved_network, Vec::<usize>::new());
1900                assert_eq!(saved_network_count_found_by_active_scan, 0);
1901            }
1902        );
1903    }
1904
1905    // Fake selector to test that ConnectionSelectionRequester and the service loop plumb requests
1906    // to the correct methods.
1907    struct FakeConnectionSelector {
1908        pub response_to_find_and_select_connection_candidate: Option<types::ScannedCandidate>,
1909        pub response_to_find_and_select_roam_candidate: Option<types::ScannedCandidate>,
1910    }
1911    #[async_trait(?Send)]
1912    impl ConnectionSelectorApi for FakeConnectionSelector {
1913        async fn find_and_select_connection_candidate(
1914            &self,
1915            _network: Option<types::NetworkIdentifier>,
1916            _reason: types::ConnectReason,
1917        ) -> Option<types::ScannedCandidate> {
1918            self.response_to_find_and_select_connection_candidate.clone()
1919        }
1920        async fn find_and_select_roam_candidate(
1921            &self,
1922            _scan_type: fidl_common::ScanType,
1923            _network: types::NetworkIdentifier,
1924            _credential: &network_config::Credential,
1925            _scanned_securioty_type: types::SecurityTypeDetailed,
1926        ) -> Option<types::ScannedCandidate> {
1927            self.response_to_find_and_select_roam_candidate.clone()
1928        }
1929    }
1930    #[fuchsia::test]
1931    fn test_service_loop_passes_connection_selection_request() {
1932        let mut exec = fasync::TestExecutor::new();
1933
1934        // Create a fake selector, since we're just testing the service loop.
1935        let candidate = generate_random_scanned_candidate();
1936        let connection_selector = Rc::new(FakeConnectionSelector {
1937            response_to_find_and_select_connection_candidate: Some(candidate.clone()),
1938            response_to_find_and_select_roam_candidate: None,
1939        });
1940
1941        // Start the service loop
1942        let (request_sender, request_receiver) = mpsc::channel(5);
1943        let mut serve_fut =
1944            pin!(serve_connection_selection_request_loop(connection_selector, request_receiver));
1945        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1946
1947        // Create a requester struct
1948        let mut requester = ConnectionSelectionRequester { sender: request_sender };
1949
1950        // Call request method
1951        let mut connection_selection_fut = pin!(
1952            requester.do_connection_selection(None, types::ConnectReason::IdleInterfaceAutoconnect)
1953        );
1954        assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Pending);
1955
1956        // Run the service loop forward
1957        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1958
1959        // Check that receiver gets expected result, confirming the request was plumbed correctly.
1960        assert_matches!(exec.run_until_stalled(&mut connection_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
1961            assert_eq!(selected_candidate, candidate);
1962        });
1963    }
1964
1965    #[fuchsia::test]
1966    fn test_service_loop_passes_roam_selection_request() {
1967        let mut exec = fasync::TestExecutor::new();
1968
1969        // Create a fake selector, since we're just testing the service loop.
1970        let candidate = generate_random_scanned_candidate();
1971        let connection_selector = Rc::new(FakeConnectionSelector {
1972            response_to_find_and_select_connection_candidate: None,
1973            response_to_find_and_select_roam_candidate: Some(candidate.clone()),
1974        });
1975
1976        // Start the service loop
1977        let (request_sender, request_receiver) = mpsc::channel(5);
1978        let mut serve_fut =
1979            pin!(serve_connection_selection_request_loop(connection_selector, request_receiver));
1980        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1981
1982        // Create a requester struct
1983        let mut requester = ConnectionSelectionRequester { sender: request_sender };
1984
1985        // Call request method
1986        let bss_desc =
1987            BssDescription::try_from(Sequestered::release(candidate.bss.bss_description.clone()))
1988                .unwrap();
1989        let mut roam_selection_fut = pin!(requester.do_roam_selection(
1990            fidl_common::ScanType::Passive,
1991            candidate.network.clone(),
1992            candidate.credential.clone(),
1993            bss_desc.protection().into()
1994        ));
1995        assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Pending);
1996
1997        // Run the service loop forward
1998        assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
1999
2000        // Check that receiver gets expected result, confirming the request was plumbed correctly.
2001        assert_matches!(exec.run_until_stalled(&mut roam_selection_fut), Poll::Ready(Ok(Some(selected_candidate))) => {
2002            assert_eq!(selected_candidate, candidate);
2003        });
2004    }
2005}