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