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