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                .filter(|c| &c.credential == credential)
462            {
463                candidates.append(&mut merge_config_and_scan_data(config, &mut s));
464            } else {
465                // This should only happen if the network config was just removed as the scan
466                // happened.
467                warn!("Failed to find config for network to roam from");
468            }
469        }
470        // Choose the best AP
471        bss_selection::select_bss(
472            candidates,
473            types::ConnectReason::ProactiveNetworkSwitch,
474            self.inspect_node_for_connection_selection.clone(),
475            self.telemetry_sender.clone(),
476        )
477        .await
478    }
479}
480
481impl types::ScannedCandidate {
482    pub fn recent_failure_count(&self) -> u64 {
483        self.saved_network_info
484            .recent_failures
485            .iter()
486            .filter(|failure| failure.bssid == self.bss.bssid)
487            .count()
488            .try_into()
489            .unwrap_or_else(|e| {
490                error!("{}", e);
491                u64::MAX
492            })
493    }
494    pub fn recent_short_connections(&self) -> usize {
495        self.saved_network_info
496            .past_connections
497            .get_list_for_bss(&self.bss.bssid)
498            .get_recent(fasync::MonotonicInstant::now() - RECENT_DISCONNECT_WINDOW)
499            .iter()
500            .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION)
501            .count()
502    }
503
504    pub fn saved_security_type_to_string(&self) -> String {
505        match self.network.security_type {
506            SecurityType::None => "open",
507            SecurityType::Wep => "WEP",
508            SecurityType::Wpa => "WPA1",
509            SecurityType::Wpa2 => "WPA2",
510            SecurityType::Wpa3 => "WPA3",
511        }
512        .to_string()
513    }
514
515    pub fn scanned_security_type_to_string(&self) -> String {
516        match self.security_type_detailed {
517            SecurityTypeDetailed::Unknown => "unknown",
518            SecurityTypeDetailed::Open => "open",
519            SecurityTypeDetailed::OpenOweTransition => "Open OWE Transition",
520            SecurityTypeDetailed::Owe => "OWE",
521            SecurityTypeDetailed::Wep => "WEP",
522            SecurityTypeDetailed::Wpa1 => "WPA1",
523            SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk",
524            SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk",
525            SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2",
526            SecurityTypeDetailed::Wpa2Personal => "WPA2",
527            SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3",
528            SecurityTypeDetailed::Wpa3Personal => "WPA3",
529            SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent",
530            SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent",
531        }
532        .to_string()
533    }
534
535    pub fn to_string_without_pii(&self) -> String {
536        let channel = self.bss.channel;
537        let rssi = self.bss.signal.rssi_dbm;
538        let recent_failure_count = self.recent_failure_count();
539        let recent_short_connection_count = self.recent_short_connections();
540
541        format!(
542            "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}",
543            self.network.ssid,
544            self.saved_security_type_to_string(),
545            self.bss.bssid,
546            self.scanned_security_type_to_string(),
547            rssi,
548            channel,
549            scoring_functions::score_bss_scanned_candidate(self.clone()),
550            if !self.bss.is_compatible() { ", NOT compatible" } else { "" },
551            if recent_failure_count > 0 {
552                format!(", {recent_failure_count} recent failures")
553            } else {
554                "".to_string()
555            },
556            if recent_short_connection_count > 0 {
557                format!(", {recent_short_connection_count} recent short disconnects")
558            } else {
559                "".to_string()
560            },
561            if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" },
562        )
563    }
564}
565
566impl WriteInspect for types::ScannedCandidate {
567    fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
568        inspect_insert!(writer, var key: {
569            ssid: self.network.ssid.to_string(),
570            bssid: self.bss.bssid.to_string(),
571            rssi: self.bss.signal.rssi_dbm,
572            score: scoring_functions::score_bss_scanned_candidate(self.clone()),
573            security_type_saved: self.saved_security_type_to_string(),
574            security_type_scanned: format!(
575                "{}",
576                wlan_common::bss::Protection::from(self.security_type_detailed),
577            ),
578            channel: format!("{}", self.bss.channel),
579            compatible: self.bss.is_compatible(),
580            incompatibility: self.bss
581                .compatibility
582                .as_ref()
583                .err()
584                .map(ToString::to_string)
585                .unwrap_or_else(|| String::from("none")),
586            recent_failure_count: self.recent_failure_count(),
587            saved_network_has_ever_connected: self.saved_network_info.has_ever_connected,
588        });
589    }
590}
591
592fn get_authenticator(bss: &Bss, credential: &Credential) -> Option<SecurityAuthenticator> {
593    let mutual_security_protocols = match bss.compatibility.as_ref() {
594        Ok(compatible) => compatible.mutual_security_protocols().clone(),
595        Err(incompatible) => {
596            error!("BSS ({:?}) is incompatible: {}", bss.bssid, incompatible);
597            return None;
598        }
599    };
600
601    match select_authentication_method(mutual_security_protocols.clone(), credential) {
602        Some(authenticator) => Some(authenticator),
603        None => {
604            error!(
605                "Failed to negotiate authentication for BSS ({:?}) with mutually supported \
606                 security protocols: {:?}, and credential type: {:?}.",
607                bss.bssid,
608                mutual_security_protocols,
609                credential.type_str()
610            );
611            None
612        }
613    }
614}
615
616fn merge_config_and_scan_data(
617    network_config: config_management::NetworkConfig,
618    scan_result: &mut types::ScanResult,
619) -> Vec<types::ScannedCandidate> {
620    if network_config.ssid != scan_result.ssid
621        || !network_config
622            .security_type
623            .is_compatible_with_scanned_type(&scan_result.security_type_detailed)
624    {
625        return Vec::new();
626    }
627
628    let mut merged_networks = Vec::new();
629    let multiple_bss_candidates = scan_result.entries.len() > 1;
630
631    for bss in scan_result.entries.iter() {
632        let authenticator = match get_authenticator(bss, &network_config.credential) {
633            Some(authenticator) => authenticator,
634            None => {
635                error!(
636                    "Failed to create authenticator for bss candidate {:?} (SSID: {:?}). Removing from candidates.",
637                    bss.bssid, network_config.ssid
638                );
639                continue;
640            }
641        };
642        let scanned_candidate = types::ScannedCandidate {
643            network: types::NetworkIdentifier {
644                ssid: network_config.ssid.clone(),
645                security_type: network_config.security_type,
646            },
647            security_type_detailed: scan_result.security_type_detailed,
648            credential: network_config.credential.clone(),
649            network_has_multiple_bss: multiple_bss_candidates,
650            saved_network_info: InternalSavedNetworkData {
651                has_ever_connected: network_config.has_ever_connected,
652                recent_failures: network_config.perf_stats.connect_failures.get_recent_for_network(
653                    fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
654                ),
655                past_connections: network_config.perf_stats.past_connections.clone(),
656            },
657            bss: bss.clone(),
658            authenticator,
659        };
660        merged_networks.push(scanned_candidate)
661    }
662    merged_networks
663}
664
665/// Merge the saved networks and scan results into a vector of BSS candidates that correspond to a
666/// saved network.
667async fn merge_saved_networks_and_scan_data(
668    saved_network_manager: &Arc<dyn SavedNetworksManagerApi>,
669    mut scan_results: Vec<types::ScanResult>,
670) -> Vec<types::ScannedCandidate> {
671    let mut merged_networks = vec![];
672    for mut scan_result in scan_results.drain(..) {
673        for saved_config in saved_network_manager
674            .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed)
675            .await
676        {
677            let multiple_bss_candidates = scan_result.entries.len() > 1;
678            for bss in scan_result.entries.drain(..) {
679                let authenticator = match get_authenticator(&bss, &saved_config.credential) {
680                    Some(authenticator) => authenticator,
681                    None => {
682                        error!(
683                            "Failed to create authenticator for bss candidate {} (SSID: {}). Removing from candidates.",
684                            bss.bssid, saved_config.ssid
685                        );
686                        continue;
687                    }
688                };
689                let scanned_candidate = types::ScannedCandidate {
690                    network: types::NetworkIdentifier {
691                        ssid: saved_config.ssid.clone(),
692                        security_type: saved_config.security_type,
693                    },
694                    security_type_detailed: scan_result.security_type_detailed,
695                    credential: saved_config.credential.clone(),
696                    network_has_multiple_bss: multiple_bss_candidates,
697                    saved_network_info: InternalSavedNetworkData {
698                        has_ever_connected: saved_config.has_ever_connected,
699                        recent_failures: saved_config
700                            .perf_stats
701                            .connect_failures
702                            .get_recent_for_network(
703                                fasync::MonotonicInstant::now() - RECENT_FAILURE_WINDOW,
704                            ),
705                        past_connections: saved_config.perf_stats.past_connections.clone(),
706                    },
707                    bss,
708                    authenticator,
709                };
710                merged_networks.push(scanned_candidate)
711            }
712        }
713    }
714    merged_networks
715}
716
717fn record_metrics_on_scan(
718    mut merged_networks: Vec<types::ScannedCandidate>,
719    telemetry_sender: &TelemetrySender,
720) {
721    let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<types::ScannedCandidate>> =
722        HashMap::new();
723    for bss in merged_networks.drain(..) {
724        merged_network_map.entry(bss.network.clone()).or_default().push(bss);
725    }
726
727    let num_saved_networks_observed = merged_network_map.len();
728    let mut num_actively_scanned_networks = 0;
729    let bss_count_per_saved_network = merged_network_map
730        .values()
731        .map(|bsss| {
732            // Check if the network was found via active scan.
733            if bsss.iter().any(|bss| matches!(bss.bss.observation, types::ScanObservation::Active))
734            {
735                num_actively_scanned_networks += 1;
736            };
737            // Count how many BSSs are visible in the scan results for this saved network.
738            bsss.len()
739        })
740        .collect();
741
742    telemetry_sender.send(TelemetryEvent::ConnectionSelectionScanResults {
743        saved_network_count: num_saved_networks_observed,
744        bss_count_per_saved_network,
745        saved_network_count_found_by_active_scan: num_actively_scanned_networks,
746    });
747}
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use crate::config_management::network_config::HistoricalListsByBssid;
752    use crate::config_management::{ConnectFailure, FailureReason, SavedNetworksManager};
753    use crate::util::testing::fakes::{FakeSavedNetworksManager, FakeScanRequester};
754    use crate::util::testing::{
755        generate_channel, generate_random_bss, generate_random_connect_reason,
756        generate_random_network_identifier, generate_random_password, generate_random_scan_result,
757        generate_random_scanned_candidate,
758    };
759    use assert_matches::assert_matches;
760    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
761    use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
762    use fidl_fuchsia_wlan_sme as fidl_sme;
763    use fuchsia_async as fasync;
764    use fuchsia_inspect as inspect;
765    use futures::task::Poll;
766    use ieee80211_testutils::BSSID_REGEX;
767    use rand::Rng;
768    use std::pin::pin;
769    use std::rc::Rc;
770    use std::sync::LazyLock;
771    use test_case::test_case;
772    use wlan_common::bss::BssDescription;
773    use wlan_common::random_fidl_bss_description;
774    use wlan_common::scan::Compatible;
775    use wlan_common::security::SecurityDescriptor;
776
777    pub static TEST_PASSWORD: LazyLock<Credential> =
778        LazyLock::new(|| Credential::Password(b"password".to_vec()));
779
780    struct TestValues {
781        connection_selector: Rc<ConnectionSelector>,
782        real_saved_network_manager: Arc<dyn SavedNetworksManagerApi>,
783        saved_network_manager: Arc<FakeSavedNetworksManager>,
784        scan_requester: Arc<FakeScanRequester>,
785        inspector: inspect::Inspector,
786        telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
787    }
788
789    async fn test_setup(use_real_save_network_manager: bool) -> TestValues {
790        let real_saved_network_manager = Arc::new(SavedNetworksManager::new_for_test().await);
791        let saved_network_manager = Arc::new(FakeSavedNetworksManager::new());
792        let scan_requester = Arc::new(FakeScanRequester::new());
793        let inspector = inspect::Inspector::default();
794        let inspect_node = inspector.root().create_child("connection_selection_test");
795        let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
796
797        let connection_selector = Rc::new(ConnectionSelector::new(
798            if use_real_save_network_manager {
799                real_saved_network_manager.clone()
800            } else {
801                saved_network_manager.clone()
802            },
803            scan_requester.clone(),
804            inspect_node,
805            TelemetrySender::new(telemetry_sender),
806        ));
807
808        TestValues {
809            connection_selector,
810            real_saved_network_manager,
811            saved_network_manager,
812            scan_requester,
813            inspector,
814            telemetry_receiver,
815        }
816    }
817
818    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
819        fidl_sme::ConnectResult {
820            code: fidl_ieee80211::StatusCode::Success,
821            is_credential_rejected: false,
822            is_reconnect: false,
823        }
824    }
825
826    #[fuchsia::test]
827    async fn scan_results_merged_with_saved_networks() {
828        let test_values = test_setup(true).await;
829
830        // create some identifiers
831        let test_ssid_1 = types::Ssid::try_from("foo").unwrap();
832        let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal;
833        let test_id_1 = types::NetworkIdentifier {
834            ssid: test_ssid_1.clone(),
835            security_type: types::SecurityType::Wpa3,
836        };
837        let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
838        let test_ssid_2 = types::Ssid::try_from("bar").unwrap();
839        let test_security_2 = types::SecurityTypeDetailed::Wpa1;
840        let test_id_2 = types::NetworkIdentifier {
841            ssid: test_ssid_2.clone(),
842            security_type: types::SecurityType::Wpa,
843        };
844        let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
845
846        // insert the saved networks
847        assert!(
848            test_values
849                .real_saved_network_manager
850                .store(test_id_1.clone(), credential_1.clone())
851                .await
852                .unwrap()
853                .is_none()
854        );
855
856        assert!(
857            test_values
858                .real_saved_network_manager
859                .store(test_id_2.clone(), credential_2.clone())
860                .await
861                .unwrap()
862                .is_none()
863        );
864
865        // build some scan results
866        let mock_scan_results = vec![
867            types::ScanResult {
868                ssid: test_ssid_1.clone(),
869                security_type_detailed: test_security_1,
870                entries: vec![
871                    types::Bss {
872                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
873                        ..generate_random_bss()
874                    },
875                    types::Bss {
876                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
877                        ..generate_random_bss()
878                    },
879                    types::Bss {
880                        compatibility: Compatible::expect_ok([SecurityDescriptor::WPA3_PERSONAL]),
881                        ..generate_random_bss()
882                    },
883                ],
884                compatibility: types::Compatibility::Supported,
885            },
886            types::ScanResult {
887                ssid: test_ssid_2.clone(),
888                security_type_detailed: test_security_2,
889                entries: vec![types::Bss {
890                    compatibility: Compatible::expect_ok([SecurityDescriptor::WPA1]),
891                    ..generate_random_bss()
892                }],
893                compatibility: types::Compatibility::DisallowedNotSupported,
894            },
895        ];
896
897        let bssid_1 = mock_scan_results[0].entries[0].bssid;
898        let bssid_2 = mock_scan_results[0].entries[1].bssid;
899
900        // mark the first one as having connected
901        test_values
902            .real_saved_network_manager
903            .record_connect_result(
904                test_id_1.clone(),
905                &credential_1.clone(),
906                bssid_1,
907                fake_successful_connect_result(),
908                types::ScanObservation::Unknown,
909            )
910            .await;
911
912        // mark the second one as having a failure
913        test_values
914            .real_saved_network_manager
915            .record_connect_result(
916                test_id_1.clone(),
917                &credential_1.clone(),
918                bssid_2,
919                fidl_sme::ConnectResult {
920                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
921                    is_credential_rejected: true,
922                    ..fake_successful_connect_result()
923                },
924                types::ScanObservation::Unknown,
925            )
926            .await;
927
928        // build our expected result
929        let failure_time = test_values
930            .real_saved_network_manager
931            .lookup(&test_id_1.clone())
932            .await
933            .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, fidl_fuchsia_wlan_ieee80211::WlanBand::TwoGhz);
1451        let mutual_security_protocols_2 = [SecurityDescriptor::WPA1];
1452        let channel_2 = generate_channel(50, fidl_fuchsia_wlan_ieee80211::WlanBand::FiveGhz);
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}