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