Skip to main content

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