Skip to main content

wlancfg_lib/client/
state_machine.rs

1// Copyright 2021 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::roaming::lib::{PolicyRoamRequest, ROAMING_CHANNEL_BUFFER_SIZE};
6use crate::client::roaming::local_roam_manager::RoamManager;
7use crate::client::roaming::roam_monitor::RoamDataSender;
8use crate::client::types;
9use crate::config_management::{Credential, PastConnectionData, SavedNetworksManagerApi};
10use crate::mode_management::iface_manager_api::SmeForClientStateMachine;
11use crate::mode_management::{Defect, IfaceFailure};
12use crate::telemetry::{
13    AVERAGE_SCORE_DELTA_MINIMUM_DURATION, DisconnectInfo, METRICS_SHORT_CONNECT_DURATION,
14    TelemetryEvent, TelemetrySender,
15};
16use crate::util::listener::Message::NotifyListeners;
17use crate::util::listener::{ClientListenerMessageSender, ClientNetworkState, ClientStateUpdate};
18use crate::util::state_machine::{self, ExitReason, IntoStateExt, StateMachineStatusPublisher};
19use anyhow::format_err;
20use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
21use fidl_fuchsia_wlan_internal as fidl_internal;
22use fidl_fuchsia_wlan_policy as fidl_policy;
23use fidl_fuchsia_wlan_sme as fidl_sme;
24use fuchsia_async::{self as fasync, DurationExt};
25use fuchsia_inspect::Node as InspectNode;
26use fuchsia_inspect_contrib::inspect_insert;
27use fuchsia_inspect_contrib::log::WriteInspect;
28use futures::channel::{mpsc, oneshot};
29use futures::future::{Fuse, FutureExt};
30use futures::select;
31use futures::stream::{self, StreamExt, TryStreamExt};
32use log::{debug, error, info, warn};
33use std::borrow::Cow;
34use std::pin::Pin;
35use std::sync::Arc;
36use wlan_common::bss::BssDescription;
37use wlan_common::channel::{Bandwidth, Channel};
38use wlan_common::historical_list::HistoricalList;
39use wlan_common::sequestered::Sequestered;
40
41const MAX_CONNECTION_ATTEMPTS: u8 = 4; // arbitrarily chosen until we have some data
42const NUM_PAST_SCORES: usize = 91; // number of past periodic connection scores to store for metrics
43const PENDING_ROAM_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(3);
44
45type State = state_machine::State<ExitReason>;
46type ReqStream = stream::Fuse<mpsc::Receiver<ManualRequest>>;
47pub type TrackedSignals = HistoricalList<types::TimestampedSignal, NUM_PAST_SCORES>;
48
49#[derive(Clone)]
50struct PendingRoam {
51    pub request: PolicyRoamRequest,
52    pub timestamp: fasync::MonotonicInstant,
53}
54impl From<PolicyRoamRequest> for PendingRoam {
55    fn from(request: PolicyRoamRequest) -> Self {
56        Self { request, timestamp: fasync::MonotonicInstant::now() }
57    }
58}
59
60pub trait ClientApi {
61    fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error>;
62    fn disconnect(
63        &mut self,
64        reason: types::DisconnectReason,
65        responder: oneshot::Sender<()>,
66    ) -> Result<(), anyhow::Error>;
67
68    /// Queries the liveness of the channel used to control the client state machine.  If the
69    /// channel is not alive, this indicates that the client state machine has exited.
70    fn is_alive(&self) -> bool;
71}
72
73pub struct Client {
74    req_sender: mpsc::Sender<ManualRequest>,
75}
76
77impl Client {
78    pub fn new(req_sender: mpsc::Sender<ManualRequest>) -> Self {
79        Self { req_sender }
80    }
81}
82
83impl ClientApi for Client {
84    fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error> {
85        self.req_sender
86            .try_send(ManualRequest::Connect(Box::new(selection)))
87            .map_err(|e| format_err!("failed to send connect selection: {:?}", e))
88    }
89
90    fn disconnect(
91        &mut self,
92        reason: types::DisconnectReason,
93        responder: oneshot::Sender<()>,
94    ) -> Result<(), anyhow::Error> {
95        self.req_sender
96            .try_send(ManualRequest::Disconnect((reason, responder)))
97            .map_err(|e| format_err!("failed to send disconnect request: {:?}", e))
98    }
99
100    fn is_alive(&self) -> bool {
101        !self.req_sender.is_closed()
102    }
103}
104
105// TODO(https://fxbug.dev/324167674): fix.
106pub enum ManualRequest {
107    Connect(Box<types::ConnectSelection>),
108    Disconnect((types::DisconnectReason, oneshot::Sender<()>)),
109}
110
111#[derive(Clone, Debug, Default, PartialEq)]
112pub enum Status {
113    Disconnecting,
114    #[default]
115    Disconnected,
116    Connecting,
117    Connected {
118        channel: u8,
119        rssi: i8,
120        snr: i8,
121    },
122}
123
124impl Status {
125    fn from_ap_state(ap_state: &types::ApState) -> Self {
126        Status::Connected {
127            channel: ap_state.tracked.channel.primary,
128            rssi: ap_state.tracked.signal.rssi_dbm,
129            snr: ap_state.tracked.signal.snr_db,
130        }
131    }
132}
133
134impl WriteInspect for Status {
135    fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
136        match self {
137            Status::Connected { channel, rssi, snr } => {
138                inspect_insert!(writer, var key: {
139                    Connected: {
140                        channel: channel,
141                        rssi: rssi,
142                        snr: snr
143                    }
144                })
145            }
146            other => inspect_insert!(writer, var key: format!("{:?}", other)),
147        }
148    }
149}
150
151fn send_listener_state_update(
152    sender: &ClientListenerMessageSender,
153    network_update: Option<ClientNetworkState>,
154) {
155    let mut networks = vec![];
156    if let Some(network) = network_update {
157        networks.push(network)
158    }
159
160    let updates =
161        ClientStateUpdate { state: fidl_policy::WlanClientState::ConnectionsEnabled, networks };
162    match sender.clone().unbounded_send(NotifyListeners(updates)) {
163        Ok(_) => (),
164        Err(e) => error!("failed to send state update: {:?}", e),
165    };
166}
167
168pub async fn serve(
169    iface_id: u16,
170    proxy: SmeForClientStateMachine,
171    sme_event_stream: fidl_sme::ClientSmeEventStream,
172    req_stream: mpsc::Receiver<ManualRequest>,
173    update_sender: ClientListenerMessageSender,
174    saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
175    connect_selection: Option<types::ConnectSelection>,
176    telemetry_sender: TelemetrySender,
177    defect_sender: mpsc::Sender<Defect>,
178    roam_manager: RoamManager,
179    status_publisher: StateMachineStatusPublisher<Status>,
180) {
181    let next_network = connect_selection
182        .map(|selection| ConnectingOptions { connect_selection: selection, attempt_counter: 0 });
183    let disconnect_options = DisconnectingOptions {
184        disconnect_responder: None,
185        previous_network: None,
186        next_network,
187        reason: types::DisconnectReason::Startup,
188    };
189    let common_options = CommonStateOptions {
190        proxy,
191        req_stream: req_stream.fuse(),
192        update_sender,
193        saved_networks_manager,
194        telemetry_sender: telemetry_sender.clone(),
195        iface_id,
196        defect_sender,
197        roam_manager,
198        status_publisher: status_publisher.clone(),
199    };
200    let state_machine =
201        disconnecting_state(common_options, disconnect_options).into_state_machine();
202    let removal_watcher = sme_event_stream.map_ok(|_| ()).try_collect::<()>();
203    select! {
204        state_machine = state_machine.fuse() => {
205            match state_machine {
206                Err(ExitReason(Err(e))) => error!("Client state machine for iface #{} terminated with an error: {:?}",
207                    iface_id, e),
208                Err(ExitReason(Ok(_))) => info!("Client state machine for iface #{} exited gracefully",
209                    iface_id,),
210            }
211        }
212        removal_watcher = removal_watcher.fuse() => {
213            match removal_watcher {
214                Ok(()) => {
215                    info!("Device was unexpectedly removed.");
216                }
217                Err(e) => {
218                    info!("Error reading from Client SME channel of iface #{}: {:?}",
219                        iface_id, e);
220                }
221            }
222
223            telemetry_sender
224                .send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info: None });
225        },
226    }
227
228    status_publisher.publish_status(Status::Disconnected);
229}
230
231/// Common parameters passed to all states
232struct CommonStateOptions {
233    proxy: SmeForClientStateMachine,
234    req_stream: ReqStream,
235    update_sender: ClientListenerMessageSender,
236    saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
237    telemetry_sender: TelemetrySender,
238    iface_id: u16,
239    defect_sender: mpsc::Sender<Defect>,
240    roam_manager: RoamManager,
241    status_publisher: StateMachineStatusPublisher<Status>,
242}
243
244impl CommonStateOptions {
245    async fn network_is_likely_hidden(&self, options: &ConnectingOptions) -> bool {
246        match self
247            .saved_networks_manager
248            .lookup(&options.connect_selection.target.network)
249            .await
250            .filter(|config| config.credential == options.connect_selection.target.credential)
251        {
252            Some(config) => config.is_hidden(),
253            None => {
254                error!("Could not lookup if connected network is hidden.");
255                false
256            }
257        }
258    }
259}
260
261pub type ConnectionStatsSender = mpsc::UnboundedSender<fidl_internal::SignalReportIndication>;
262pub type ConnectionStatsReceiver = mpsc::UnboundedReceiver<fidl_internal::SignalReportIndication>;
263
264fn handle_none_request() -> Result<State, ExitReason> {
265    Err(ExitReason(Err(format_err!("The stream of requests ended unexpectedly"))))
266}
267
268// These functions were introduced to resolve the following error:
269// ```
270// error[E0391]: cycle detected when evaluating trait selection obligation
271// `impl core::future::future::Future: std::marker::Send`
272// ```
273// which occurs when two functions that return an `impl Trait` call each other
274// in a cycle. (e.g. this case `connecting_state` calling `disconnecting_state`,
275// which calls `connecting_state`)
276fn to_disconnecting_state(
277    common_options: CommonStateOptions,
278    disconnecting_options: DisconnectingOptions,
279) -> State {
280    disconnecting_state(common_options, disconnecting_options).into_state()
281}
282fn to_connecting_state(
283    common_options: CommonStateOptions,
284    connecting_options: ConnectingOptions,
285) -> State {
286    connecting_state(common_options, connecting_options).into_state()
287}
288
289struct DisconnectingOptions {
290    disconnect_responder: Option<oneshot::Sender<()>>,
291    /// Information about the previously connected network, if there was one. Used to send out
292    /// listener updates.
293    previous_network: Option<(types::NetworkIdentifier, types::DisconnectStatus)>,
294    /// Configuration for the next network to connect to, after the disconnect is complete. If not
295    /// present, the state machine will proceed to IDLE.
296    next_network: Option<ConnectingOptions>,
297    reason: types::DisconnectReason,
298}
299/// The DISCONNECTING state requests an SME disconnect, then transitions to either:
300/// - the CONNECTING state if options.next_network is present
301/// - exit otherwise
302async fn disconnecting_state(
303    common_options: CommonStateOptions,
304    mut options: DisconnectingOptions,
305) -> Result<State, ExitReason> {
306    // Log a message with the disconnect reason
307    match options.reason {
308        types::DisconnectReason::FailedToConnect
309        | types::DisconnectReason::Startup
310        | types::DisconnectReason::DisconnectDetectedFromSme => {
311            // These are either just noise or have separate logging, so keep the level at debug.
312            debug!("Disconnected due to {:?}", options.reason);
313        }
314        reason => {
315            info!("Disconnected due to {:?}", reason);
316        }
317    }
318
319    notify_on_disconnect_attempt(&common_options);
320
321    // TODO(https://fxbug.dev/42130926): either make this fire-and-forget in the SME, or spawn a thread for this,
322    // so we don't block on it
323    common_options
324        .proxy
325        .disconnect(types::convert_to_sme_disconnect_reason(options.reason))
326        .await
327        .map_err(|e| ExitReason(Err(e)))?;
328
329    notify_once_disconnected(&common_options, &mut options);
330
331    // Transition to next state
332    match options.next_network {
333        Some(next_network) => Ok(to_connecting_state(common_options, next_network)),
334        None => Err(ExitReason(Ok(()))),
335    }
336}
337
338fn notify_on_disconnect_attempt(common_options: &CommonStateOptions) {
339    common_options.status_publisher.publish_status(Status::Disconnecting);
340}
341
342fn notify_once_disconnected(
343    common_options: &CommonStateOptions,
344    options: &mut DisconnectingOptions,
345) {
346    common_options.status_publisher.publish_status(Status::Disconnected);
347
348    // Notify listeners if a disconnect request was sent, or ensure that listeners know client
349    // connections are enabled.
350    let networks =
351        options.previous_network.clone().map(|(network_identifier, status)| ClientNetworkState {
352            id: network_identifier,
353            state: types::ConnectionState::Disconnected,
354            status: Some(status),
355        });
356    send_listener_state_update(&common_options.update_sender, networks);
357
358    // Notify the caller that disconnect was sent to the SME once the final disconnected update has
359    // been sent.  This ensures that there will not be a race when the IfaceManager sends out a
360    // ConnectionsDisabled update.
361    #[allow(clippy::single_match, reason = "mass allow for https://fxbug.dev/381896734")]
362    match options.disconnect_responder.take() {
363        Some(responder) => responder.send(()).unwrap_or(()),
364        None => (),
365    }
366}
367
368struct ConnectingOptions {
369    connect_selection: types::ConnectSelection,
370    /// Count of previous consecutive failed connection attempts to this same network.
371    attempt_counter: u8,
372}
373
374async fn handle_connecting_error_and_retry(
375    common_options: CommonStateOptions,
376    options: ConnectingOptions,
377) -> Result<State, ExitReason> {
378    // Check if the limit for connection attempts to this network has been
379    // exceeded.
380    let new_attempt_count = options.attempt_counter + 1;
381    if new_attempt_count >= MAX_CONNECTION_ATTEMPTS {
382        info!("Exceeded maximum connection attempts, will not retry");
383        send_listener_state_update(
384            &common_options.update_sender,
385            Some(ClientNetworkState {
386                id: options.connect_selection.target.network,
387                state: types::ConnectionState::Failed,
388                status: Some(types::DisconnectStatus::ConnectionFailed),
389            }),
390        );
391        Err(ExitReason(Ok(())))
392    } else {
393        // Limit not exceeded, retry after backing off.
394        let backoff_time = 400_i64 * i64::from(new_attempt_count);
395        info!("Will attempt to reconnect after {}ms backoff", backoff_time);
396        fasync::Timer::new(zx::MonotonicDuration::from_millis(backoff_time).after_now()).await;
397
398        let next_connecting_options = ConnectingOptions {
399            connect_selection: types::ConnectSelection {
400                reason: types::ConnectReason::RetryAfterFailedConnectAttempt,
401                ..options.connect_selection
402            },
403            attempt_counter: new_attempt_count,
404        };
405        let disconnecting_options = DisconnectingOptions {
406            disconnect_responder: None,
407            previous_network: None,
408            next_network: Some(next_connecting_options),
409            reason: types::DisconnectReason::FailedToConnect,
410        };
411        Ok(to_disconnecting_state(common_options, disconnecting_options))
412    }
413}
414
415#[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
416/// The CONNECTING state requests an SME connect. It handles the SME connect response:
417/// - for a successful connection, transition to CONNECTED state
418/// - for a failed connection, retry connection by passing a next_network to the
419///   DISCONNECTING state, as long as there haven't been too many connection attempts
420#[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
421/// During this time, incoming ManualRequests are also monitored for:
422/// - duplicate connect requests are deduped
423/// - different connect requests are serviced by passing a next_network to the DISCONNECTING state
424/// - disconnect requests cause a transition to DISCONNECTING state
425async fn connecting_state(
426    mut common_options: CommonStateOptions,
427    options: ConnectingOptions,
428) -> Result<State, ExitReason> {
429    debug!("Entering connecting state");
430    notify_on_connection_attempt(&common_options, &options);
431
432    // Release the sequestered BSS description. While considered a "black box" elsewhere, the state
433    // machine uses this by design to construct its AP state and to report telemetry.
434    let bss_description =
435        Sequestered::release(options.connect_selection.target.bss.bss_description.clone());
436    let ap_state = types::ApState::from(
437        BssDescription::try_from(bss_description.clone()).map_err(|error| {
438            // This only occurs if an invalid `BssDescription` is received from SME, which should
439            // never happen.
440            ExitReason(Err(
441                format_err!("Failed to convert BSS description from FIDL: {:?}", error,),
442            ))
443        })?,
444    );
445
446    let sme_connect_request = fidl_sme::ConnectRequest {
447        ssid: options.connect_selection.target.network.ssid.to_vec(),
448        bss_description,
449        multiple_bss_candidates: options.connect_selection.target.network_has_multiple_bss,
450        authentication: options.connect_selection.target.authenticator.clone().into(),
451        deprecated_scan_type: fidl_fuchsia_wlan_common::ScanType::Active,
452    };
453    let (sme_result, connect_txn_stream) = common_options
454        .proxy
455        .connect(&sme_connect_request)
456        .await
457        .map_err(|e| ExitReason(Err(format_err!("{:?}", e))))?;
458
459    notify_on_connection_result(&mut common_options, &options, ap_state.clone(), sme_result).await;
460
461    match (sme_result.code, sme_result.is_credential_rejected) {
462        (fidl_ieee80211::StatusCode::Success, _) => {
463            info!("Successfully connected to network");
464            let network_is_likely_hidden = common_options.network_is_likely_hidden(&options).await;
465            let connected_options = ConnectedOptions::new(
466                &mut common_options,
467                Box::new(ap_state.clone()),
468                options.connect_selection.target.network_has_multiple_bss,
469                options.connect_selection.target.network.clone(),
470                options.connect_selection.target.credential.clone(),
471                options.connect_selection.reason,
472                connect_txn_stream,
473                network_is_likely_hidden,
474            );
475            Ok(connected_state(common_options, connected_options).into_state())
476        }
477        (code, true) => {
478            info!("Failed to connect: {:?}. Will not retry because of credential error.", code);
479            return Err(ExitReason(Ok(())));
480        }
481        (code, _) => {
482            info!("Failed to connect: {:?}", code);
483            handle_connecting_error_and_retry(common_options, options).await
484        }
485    }
486}
487
488fn notify_on_connection_attempt(common_options: &CommonStateOptions, options: &ConnectingOptions) {
489    common_options.status_publisher.publish_status(Status::Connecting);
490
491    if options.attempt_counter > 0 {
492        info!(
493            "Retrying connection, {} attempts remaining",
494            MAX_CONNECTION_ATTEMPTS - options.attempt_counter
495        );
496    }
497
498    // Send a "Connecting" update to listeners, unless this is a retry
499    if options.attempt_counter == 0 {
500        send_listener_state_update(
501            &common_options.update_sender,
502            Some(ClientNetworkState {
503                id: options.connect_selection.target.network.clone(),
504                state: types::ConnectionState::Connecting,
505                status: None,
506            }),
507        );
508    };
509}
510
511async fn notify_on_connection_result(
512    common_options: &mut CommonStateOptions,
513    options: &ConnectingOptions,
514    ap_state: types::ApState,
515    sme_result: fidl_sme::ConnectResult,
516) {
517    common_options.status_publisher.publish_status(Status::from_ap_state(&ap_state));
518
519    // Report the connect result to the saved networks manager.
520    common_options
521        .saved_networks_manager
522        .record_connect_result(
523            options.connect_selection.target.network.clone(),
524            &options.connect_selection.target.credential,
525            ap_state.clone().original().bssid,
526            sme_result,
527            options.connect_selection.target.bss.observation,
528        )
529        .await;
530
531    let network_is_likely_hidden = common_options.network_is_likely_hidden(options).await;
532    common_options.telemetry_sender.send(TelemetryEvent::ConnectResult {
533        ap_state,
534        result: sme_result,
535        policy_connect_reason: Some(options.connect_selection.reason),
536        multiple_bss_candidates: options.connect_selection.target.network_has_multiple_bss,
537        iface_id: common_options.iface_id,
538        network_is_likely_hidden,
539    });
540
541    if sme_result.code == fidl_ieee80211::StatusCode::Success {
542        send_listener_state_update(
543            &common_options.update_sender,
544            Some(ClientNetworkState {
545                id: options.connect_selection.target.network.clone(),
546                state: types::ConnectionState::Connected,
547                status: None,
548            }),
549        );
550    } else if sme_result.is_credential_rejected {
551        send_listener_state_update(
552            &common_options.update_sender,
553            Some(ClientNetworkState {
554                id: options.connect_selection.target.network.clone(),
555                state: types::ConnectionState::Failed,
556                status: Some(types::DisconnectStatus::CredentialsFailed),
557            }),
558        );
559    } else {
560        // Defects should be logged for connection failures that are not due to bad credentials.
561        if let Err(e) =
562            common_options.defect_sender.try_send(Defect::Iface(IfaceFailure::ConnectionFailure {
563                iface_id: common_options.iface_id,
564            }))
565        {
566            warn!("Failed to log connection failure: {}", e);
567        }
568    }
569}
570
571struct ConnectedOptions {
572    // Keep track of the BSSID we are connected in order to record connection information for
573    // future network selection.
574    ap_state: Box<types::ApState>,
575    multiple_bss_candidates: bool,
576    network_identifier: types::NetworkIdentifier,
577    credential: Credential,
578    ess_connect_reason: types::ConnectReason,
579    connect_txn_stream: fidl_sme::ConnectTransactionEventStream,
580    network_is_likely_hidden: bool,
581    ess_connect_start_time: fasync::MonotonicInstant,
582    bss_connect_start_time: fasync::MonotonicInstant,
583    initial_signal: types::Signal,
584    tracked_signals: TrackedSignals,
585    roam_monitor_sender: RoamDataSender,
586    roam_request_receiver: mpsc::Receiver<PolicyRoamRequest>,
587    post_connect_metric_timer: Pin<Box<fasync::Timer>>,
588    bss_connect_duration_metric_timer: Pin<Box<fasync::Timer>>,
589    pending_roam: Option<PendingRoam>,
590    pending_roam_timer: Pin<Box<Fuse<fasync::Timer>>>,
591}
592impl ConnectedOptions {
593    pub fn new(
594        common_options: &mut CommonStateOptions,
595        ap_state: Box<types::ApState>,
596        multiple_bss_candidates: bool,
597        network_identifier: types::NetworkIdentifier,
598        credential: Credential,
599        ess_connect_reason: types::ConnectReason,
600        connect_txn_stream: fidl_sme::ConnectTransactionEventStream,
601        network_is_likely_hidden: bool,
602    ) -> Self {
603        // Tracked signals
604        let mut past_signals = TrackedSignals::new();
605        let initial_signal = ap_state.tracked.signal;
606        past_signals.add(types::TimestampedSignal {
607            time: fasync::MonotonicInstant::now(),
608            signal: initial_signal,
609        });
610
611        // Initialize roam monitor with roam manager service.
612        let (roam_request_sender, roam_request_receiver) =
613            mpsc::channel(ROAMING_CHANNEL_BUFFER_SIZE);
614        let roam_monitor_sender = common_options.roam_manager.initialize_roam_monitor(
615            (*ap_state).clone(),
616            network_identifier.clone(),
617            credential.clone(),
618            roam_request_sender,
619        );
620        Self {
621            ap_state,
622            multiple_bss_candidates,
623            network_identifier,
624            credential,
625            ess_connect_reason,
626            connect_txn_stream,
627            network_is_likely_hidden,
628            ess_connect_start_time: fasync::MonotonicInstant::now(),
629            bss_connect_start_time: fasync::MonotonicInstant::now(),
630            initial_signal,
631            tracked_signals: TrackedSignals::new(),
632            roam_monitor_sender,
633            roam_request_receiver,
634            post_connect_metric_timer: Box::pin(fasync::Timer::new(
635                AVERAGE_SCORE_DELTA_MINIMUM_DURATION.after_now(),
636            )),
637            bss_connect_duration_metric_timer: Box::pin(fasync::Timer::new(
638                METRICS_SHORT_CONNECT_DURATION.after_now(),
639            )),
640            pending_roam: None,
641            pending_roam_timer: Box::pin(Fuse::terminated()),
642        }
643    }
644}
645/// The CONNECTED state monitors the SME status. It handles the SME status response:
646/// - if still connected to the correct network, no action
647/// - if disconnected, retry connection by passing a next_network to the
648///   DISCONNECTING state
649#[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
650/// During this time, incoming ManualRequests are also monitored for:
651/// - duplicate connect requests are deduped
652/// - different connect requests are serviced by passing a next_network to the DISCONNECTING state
653/// - disconnect requests cause a transition to DISCONNECTING state
654async fn connected_state(
655    mut common_options: CommonStateOptions,
656    mut options: ConnectedOptions,
657) -> Result<State, ExitReason> {
658    debug!("Entering connected state");
659    loop {
660        select! {
661            event = options.connect_txn_stream.next() => match event {
662                Some(Ok(event)) => {
663                    let is_sme_idle = match event {
664                        fidl_sme::ConnectTransactionEvent::OnDisconnect { info: fidl_info } => {
665                            notify_when_disconnect_detected(
666                                &common_options,
667                                &options,
668                                fidl_info,
669                            ).await;
670
671                            !fidl_info.is_sme_reconnecting
672                        }
673                        fidl_sme::ConnectTransactionEvent::OnConnectResult { result } => {
674                            let connected = result.code == fidl_ieee80211::StatusCode::Success;
675                            if connected {
676                                // This OnConnectResult should be for SME reconnecting to the same
677                                // AP, so keep the same SignalData but reset the connect start time
678                                // to track as a new connection.
679                                options.ess_connect_start_time = fasync::MonotonicInstant::now();
680                                options.bss_connect_start_time = fasync::MonotonicInstant::now();
681                            }
682                            notify_when_reconnect_detected(&common_options, &options, result);
683                            !connected
684                        }
685                        fidl_sme::ConnectTransactionEvent::OnRoamResult { result } => {
686                            if let Err(error) = handle_roam_result(&mut common_options, &mut options, &result).await {
687                                error!("Error handling roam result: {:?}. Cannot proceed with connection.", error);
688                                true
689                            } else {
690                                let connected = result.status_code == fidl_ieee80211::StatusCode::Success || result.original_association_maintained;
691                                !connected
692                            }
693                        }
694                        fidl_sme::ConnectTransactionEvent::OnSignalReport { ind } => {
695                            // Update connection data
696                            options.ap_state.tracked.signal = ind.into();
697
698                            // Update list of signals
699                            options.tracked_signals.add(types::TimestampedSignal {
700                                time: fasync::MonotonicInstant::now(),
701                                signal: ind.into(),
702                            });
703
704                            notify_on_signal_report(
705                                &common_options,
706                                &mut options,
707                                ind
708                            );
709                            false
710                        }
711                        fidl_sme::ConnectTransactionEvent::OnChannelSwitched { info } => {
712                            info!(
713                                "OnChannelSwitch received. Previous channel: {:?}, new channel: {:?}.",
714                                options.ap_state.tracked.channel.primary, info.new_primary_channel
715                            );
716
717                            let cbw = match Bandwidth::from_fidl(info.bandwidth, info.vht_secondary_80_channel.number) {
718                                Ok(cbw) => cbw,
719                                Err(e) => {
720                                    // In the event that the CBW is invalid, reuse the previous CBW
721                                    // in determining the client's channel to preserve any legacy
722                                    // behavior.
723                                    error!("Invalid CBW in ChannelSwitchInfo: {}", e);
724                                    options.ap_state.tracked.channel.bandwidth
725                                }
726                            };
727                            options.ap_state.tracked.channel = Channel::new(
728                                info.new_primary_channel.number,
729                                cbw,
730                                info.new_primary_channel.band
731                            );
732
733                            // Re-initialize roam monitor for new channel
734                            let (sender, roam_request_receiver) = mpsc::channel(ROAMING_CHANNEL_BUFFER_SIZE);
735                            options.roam_request_receiver = roam_request_receiver;
736                            options.roam_monitor_sender =
737                                common_options.roam_manager.initialize_roam_monitor(
738                                    (*options.ap_state).clone(),
739                                    options.network_identifier.clone(),
740                                    options.credential.clone(),
741                                    sender
742                                );
743                            notify_on_channel_switch(&common_options, &options, info);
744                            false
745                        }
746                    };
747
748                    if is_sme_idle {
749                        info!("Idle sme detected.");
750                        let options = DisconnectingOptions {
751                            disconnect_responder: None,
752                            previous_network: Some((
753                                options.network_identifier.clone(),
754                                types::DisconnectStatus::ConnectionFailed
755                            )),
756                            next_network: None,
757                            reason: types::DisconnectReason::DisconnectDetectedFromSme,
758                        };
759                        return Ok(disconnecting_state(common_options, options).into_state());
760                    }
761                }
762                _ => {
763                    info!("SME dropped ConnectTransaction channel. Exiting state machine");
764                    return Err(ExitReason(Err(format_err!("Failed to receive ConnectTransactionEvent for SME status"))));
765                }
766            },
767            req = common_options.req_stream.next() => {
768                match req {
769                    Some(ManualRequest::Disconnect((reason, responder))) => {
770                        debug!("Disconnect requested");
771                        notify_on_manual_disconnect_request_received(
772                            &common_options,
773                            &options,
774                            reason,
775                        ).await;
776
777                        let options = DisconnectingOptions {
778                            disconnect_responder: Some(responder),
779                            previous_network: Some((
780                                options.network_identifier.clone(),
781                                types::DisconnectStatus::ConnectionStopped
782                            )),
783                            next_network: None,
784                            reason,
785                        };
786                        return Ok(disconnecting_state(common_options, options).into_state());
787                    }
788                    Some(ManualRequest::Connect(box_connect_selection)) => {
789                        let new_connect_selection = *box_connect_selection;
790                        // Check if it's the same network as we're currently connected to. If yes, reply immediately
791                        if new_connect_selection.target.network
792                            == options.network_identifier {
793                            info!("Received connection request for current network, deduping");
794                            continue
795                        }
796
797                        let reason = convert_manual_connect_to_disconnect_reason(
798                            &new_connect_selection.reason
799                        ).unwrap_or_else(|_| {
800                            error!("Unexpected connection reason: {:?}", new_connect_selection.reason);
801                            types::DisconnectReason::Unknown
802                        });
803                        notify_on_manual_connect_request_received(
804                            &common_options,
805                            &options,
806                            reason,
807                        ).await;
808
809                        let options = DisconnectingOptions {
810                            disconnect_responder: None,
811                            previous_network: Some((
812                                options.network_identifier,
813                                types::DisconnectStatus::ConnectionStopped
814                            )),
815                            next_network: Some(ConnectingOptions {
816                                connect_selection: new_connect_selection.clone(),
817                                attempt_counter: 0,
818                            }),
819                            reason
820                        };
821                        info!("Connection to new network requested, disconnecting from current network");
822                        return Ok(disconnecting_state(common_options, options).into_state())
823                    }
824                    None => return handle_none_request(),
825                };
826            },
827            () = &mut options.post_connect_metric_timer => {
828                common_options.telemetry_sender.send(TelemetryEvent::PostConnectionSignals {
829                        connect_time: options.bss_connect_start_time,
830                        signal_at_connect: options.initial_signal,
831                        signals: options.tracked_signals.clone()
832                });
833            },
834            () = &mut options.bss_connect_duration_metric_timer => {
835                // Log the average connection score metric for a long duration BSS connection.
836                common_options.telemetry_sender.send(TelemetryEvent::LongDurationSignals{
837                    signals: options.tracked_signals.get_before(fasync::MonotonicInstant::now())
838                });
839            },
840            () = &mut options.pending_roam_timer => {
841                error!("Pending roam request has timed out without a response from SME, cannot proceed with connection");
842                notify_on_roam_error_and_exit(&common_options, &options).await;
843                let options = DisconnectingOptions {
844                    disconnect_responder: None,
845                    previous_network: Some((
846                        options.network_identifier.clone(),
847                        types::DisconnectStatus::ConnectionFailed
848                    )),
849                    next_network: None,
850                    // TODO(b/405151253): Add a disconnect reason for errors attempting to roam.
851                    reason: types::DisconnectReason::DisconnectDetectedFromSme,
852                };
853                return Ok(disconnecting_state(common_options, options).into_state());
854            }
855            roam_request = options.roam_request_receiver.select_next_some() => {
856                if let Some(pending_roam) = &options.pending_roam {
857                    info!("Already pending a roam result for a requested roam to BSSID: {:?}", pending_roam.request.candidate.bss.bssid);
858                } else {
859                    let _ = common_options
860                    .proxy
861                    .roam(&roam_request.clone().into())
862                    .inspect_err(|e| {
863                        error!("Error sending sme roam request: {}", e);
864                    });
865                    options.pending_roam = Some(roam_request.clone().into());
866                    options.pending_roam_timer.set(fasync::Timer::new(PENDING_ROAM_TIMEOUT.after_now()).fuse());
867                    common_options.telemetry_sender.send(TelemetryEvent::PolicyRoamAttempt {
868                        request: roam_request,
869                        connected_duration: fasync::MonotonicInstant::now() - options.bss_connect_start_time,
870                    });
871                }
872            }
873        }
874    }
875}
876
877async fn notify_when_disconnect_detected(
878    common_options: &CommonStateOptions,
879    options: &ConnectedOptions,
880    fidl_info: fidl_sme::DisconnectInfo,
881) {
882    log_disconnect_to_telemetry(common_options, options, fidl_info, true).await;
883    log_disconnect_to_config_manager(
884        common_options,
885        options,
886        types::DisconnectReason::DisconnectDetectedFromSme,
887    )
888    .await
889}
890
891fn notify_when_reconnect_detected(
892    common_options: &CommonStateOptions,
893    options: &ConnectedOptions,
894    result: fidl_sme::ConnectResult,
895) {
896    common_options.telemetry_sender.send(TelemetryEvent::ConnectResult {
897        iface_id: common_options.iface_id,
898        result,
899        policy_connect_reason: None,
900        // It's not necessarily true that there are still multiple BSS
901        // candidates in the network at this point in time, but we use the
902        // heuristic that if previously there were multiple BSS's, then
903        // it likely remains the same.
904        multiple_bss_candidates: options.multiple_bss_candidates,
905        ap_state: (*options.ap_state).clone(),
906        network_is_likely_hidden: options.network_is_likely_hidden,
907    });
908}
909
910fn notify_on_signal_report(
911    common_options: &CommonStateOptions,
912    options: &mut ConnectedOptions,
913    ind: fidl_internal::SignalReportIndication,
914) {
915    // Update reported state.
916    common_options.status_publisher.publish_status(Status::from_ap_state(&options.ap_state));
917
918    // Send signal report metrics
919    common_options.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind });
920
921    // Forward signal report data to roam monitor.
922    let _ = options
923        .roam_monitor_sender
924        .send_signal_report_ind(ind)
925        .inspect_err(|e| error!("Error handling signal report: {}", e));
926}
927
928fn notify_on_channel_switch(
929    common_options: &CommonStateOptions,
930    options: &ConnectedOptions,
931    info: fidl_internal::ChannelSwitchInfo,
932) {
933    common_options.telemetry_sender.send(TelemetryEvent::OnChannelSwitched { info });
934    // Update reported state.
935    common_options.status_publisher.publish_status(Status::from_ap_state(&options.ap_state));
936}
937
938async fn notify_on_manual_disconnect_request_received(
939    common_options: &CommonStateOptions,
940    options: &ConnectedOptions,
941    reason: types::DisconnectReason,
942) {
943    let fidl_info = fidl_sme::DisconnectInfo {
944        is_sme_reconnecting: false,
945        disconnect_source: fidl_sme::DisconnectSource::User(
946            types::convert_to_sme_disconnect_reason(reason),
947        ),
948    };
949    log_disconnect_to_telemetry(common_options, options, fidl_info, false).await;
950    log_disconnect_to_config_manager(common_options, options, reason).await
951}
952
953async fn notify_on_manual_connect_request_received(
954    common_options: &CommonStateOptions,
955    options: &ConnectedOptions,
956    reason: types::DisconnectReason,
957) {
958    let fidl_info = fidl_sme::DisconnectInfo {
959        is_sme_reconnecting: false,
960        disconnect_source: fidl_sme::DisconnectSource::User(
961            types::convert_to_sme_disconnect_reason(reason),
962        ),
963    };
964    log_disconnect_to_telemetry(common_options, options, fidl_info, false).await;
965    log_disconnect_to_config_manager(common_options, options, reason).await
966}
967
968/// On a roam success:
969///   - logs a disconnect from the original BSS to saved networks manager
970///   - updates the state machine internal state (see update_internal_state_on_roam_success)
971///
972/// On a roam failure and original association not maintained:
973///   - logs a disconnect from the original BSS to saved networks manager
974///   - logs a disconnect to telemetry
975///
976/// Then the connect attempt, roam result, and any defects are logged (see notify_on_roam_result)
977async fn handle_roam_result(
978    common_options: &mut CommonStateOptions,
979    options: &mut ConnectedOptions,
980    result: &fidl_sme::RoamResult,
981) -> Result<(), anyhow::Error> {
982    // TODO(b/399649753): Distinguish between policy-initaited and firmware-initiated RoamResults
983    // here, when possible. Only correlate policy-initiated RoamResults with pending roams.
984
985    // Verify that the received roam result matches the pending roam request. If there is not a
986    // pending roam request, or the pending request is for another BSS, the connection cannot
987    // proceed.
988    let pending_roam = match options.pending_roam.take() {
989        None => {
990            notify_on_roam_error_and_exit(common_options, options).await;
991            return Err(format_err!(
992                "Roam result unexpectedly received without a pending roam request."
993            ));
994        }
995        Some(pending_roam) => {
996            if pending_roam.request.candidate.bss.bssid != result.bssid.into() {
997                notify_on_roam_error_and_exit(common_options, options).await;
998                return Err(format_err!(
999                    "Roam result received for bssid {:?} while awaiting result for bssid {:?}.",
1000                    result.bssid,
1001                    pending_roam.request.candidate.bss.bssid
1002                ));
1003            }
1004            pending_roam
1005        }
1006    };
1007    options.pending_roam_timer.set(Fuse::terminated());
1008
1009    let roam_succeeded = result.status_code == fidl_ieee80211::StatusCode::Success;
1010    let original_ap_state = options.ap_state.clone();
1011    if roam_succeeded {
1012        // Record BSS disconnect to config manager. We do not report a disconnect to
1013        // telemetry, since we are still connected to the same ESS.
1014        log_disconnect_to_config_manager(common_options, options, types::DisconnectReason::Unknown)
1015            .await;
1016        // Update internals of connected state to proceed with connection.
1017        update_internal_state_on_roam_success(common_options, options, result)?;
1018        info!("Roam succeeded!");
1019    } else if !result.original_association_maintained {
1020        // RoamResult should always include disconnect_info on failure, but this check prevents
1021        // issues if it's missing.
1022        let sme_disconnect_info = match result.disconnect_info {
1023            Some(ref info) => *info.clone(),
1024            None => {
1025                warn!("RoamResult failure does not contain SME disconnect info.");
1026                fidl_sme::DisconnectInfo {
1027                    is_sme_reconnecting: false,
1028                    disconnect_source: fidl_sme::DisconnectSource::Mlme(
1029                        fidl_sme::DisconnectCause {
1030                            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1031                            mlme_event_name:
1032                                fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1033                        },
1034                    ),
1035                }
1036            }
1037        };
1038        // Record a disconnect to both config manager and telemetry.
1039        log_disconnect_to_telemetry(common_options, options, sme_disconnect_info, true).await;
1040        log_disconnect_to_config_manager(common_options, options, types::DisconnectReason::Unknown)
1041            .await;
1042        info!("Roam attempt failed, original association not maintained, disconnecting");
1043    }
1044    notify_on_roam_result(common_options, options, result, *original_ap_state, pending_roam).await;
1045    Ok(())
1046}
1047
1048/// Called for each roam result, regardless of success/failure, after any required
1049/// internal state updates have been made. This function:
1050///   - Records the connect attempt to the target BSS with saved networks manager
1051///   - Logs the roam result to telemetry
1052///   - Publishes the current ap state (which may have been updates)
1053///   - Logs a connect failure defect, if applicable.
1054async fn notify_on_roam_result(
1055    common_options: &mut CommonStateOptions,
1056    options: &mut ConnectedOptions,
1057    result: &fidl_sme::RoamResult,
1058    original_ap_state: types::ApState,
1059    pending_roam: PendingRoam,
1060) {
1061    // Record connect result to config manager, regardless of status.
1062    common_options
1063        .saved_networks_manager
1064        .record_connect_result(
1065            options.network_identifier.clone(),
1066            &options.credential,
1067            result.bssid.into(),
1068            fidl_sme::ConnectResult {
1069                code: result.status_code,
1070                is_credential_rejected: result.is_credential_rejected,
1071                is_reconnect: match result.disconnect_info {
1072                    Some(ref info) => info.is_sme_reconnecting,
1073                    None => false,
1074                },
1075            },
1076            types::ScanObservation::Unknown,
1077        )
1078        .await;
1079
1080    // Log policy-initiated roam result to telemetry once state is up to date.
1081    common_options.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
1082        iface_id: common_options.iface_id,
1083        result: result.clone(),
1084        updated_ap_state: (*options.ap_state).clone(),
1085        original_ap_state: Box::new(original_ap_state),
1086        request: Box::new(pending_roam.request.clone()),
1087        request_time: pending_roam.timestamp,
1088        result_time: fasync::MonotonicInstant::now(),
1089    });
1090
1091    // Publish state.
1092    common_options.status_publisher.publish_status(Status::from_ap_state(&options.ap_state));
1093
1094    // Log defect on connect failure.
1095    if result.status_code != fidl_ieee80211::StatusCode::Success
1096        && !result.is_credential_rejected
1097        && let Err(e) =
1098            common_options.defect_sender.try_send(Defect::Iface(IfaceFailure::ConnectionFailure {
1099                iface_id: common_options.iface_id,
1100            }))
1101    {
1102        warn!("Failed to log connection failure: {}", e);
1103    }
1104}
1105
1106async fn notify_on_roam_error_and_exit(
1107    common_options: &CommonStateOptions,
1108    options: &ConnectedOptions,
1109) {
1110    // TODO(b/405151253): Add a disconnect reason for errors attempting to roam.
1111    let fidl_info = fidl_sme::DisconnectInfo {
1112        is_sme_reconnecting: false,
1113        disconnect_source: fidl_sme::DisconnectSource::User(
1114            fidl_sme::UserDisconnectReason::Unknown,
1115        ),
1116    };
1117    log_disconnect_to_telemetry(common_options, options, fidl_info, false).await;
1118    log_disconnect_to_config_manager(common_options, options, types::DisconnectReason::Unknown)
1119        .await;
1120
1121    // Clear the network from the updates stream, since this state machine will exit.
1122    let networks = Some(ClientNetworkState {
1123        id: options.network_identifier.clone(),
1124        state: types::ConnectionState::Disconnected,
1125        status: Some(types::DisconnectStatus::ConnectionFailed),
1126    });
1127    send_listener_state_update(&common_options.update_sender, networks);
1128}
1129
1130async fn log_disconnect_to_telemetry(
1131    common_options: &CommonStateOptions,
1132    options: &ConnectedOptions,
1133    fidl_info: fidl_sme::DisconnectInfo,
1134    track_subsequent_downtime: bool,
1135) {
1136    let now = fasync::MonotonicInstant::now();
1137    let info = DisconnectInfo {
1138        iface_id: common_options.iface_id,
1139        connected_duration: now - options.ess_connect_start_time,
1140        is_sme_reconnecting: fidl_info.is_sme_reconnecting,
1141        disconnect_source: fidl_info.disconnect_source,
1142        previous_connect_reason: options.ess_connect_reason,
1143        ap_state: (*options.ap_state).clone(),
1144        signals: options.tracked_signals.clone(),
1145    };
1146    common_options
1147        .telemetry_sender
1148        .send(TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) });
1149}
1150
1151async fn log_disconnect_to_config_manager(
1152    common_options: &CommonStateOptions,
1153    options: &ConnectedOptions,
1154    reason: types::DisconnectReason,
1155) {
1156    let curr_time = fasync::MonotonicInstant::now();
1157    let uptime = curr_time - options.bss_connect_start_time;
1158    let data = PastConnectionData::new(
1159        options.ap_state.original().bssid,
1160        curr_time,
1161        uptime,
1162        reason,
1163        options.ap_state.tracked.signal,
1164        // TODO: record average phy rate over connection once available
1165        0,
1166    );
1167    common_options
1168        .saved_networks_manager
1169        .record_disconnect(&options.network_identifier.clone(), &options.credential, data)
1170        .await;
1171}
1172
1173/// Updates all internal state following a roam to a new BSS. This includes updating the ap state,
1174/// restarting metrics timers, clearing signal tracking, and re-initializing a new roam monitor.
1175fn update_internal_state_on_roam_success(
1176    common_options: &mut CommonStateOptions,
1177    options: &mut ConnectedOptions,
1178    result: &fidl_sme::RoamResult,
1179) -> Result<(), anyhow::Error> {
1180    // Update internal state, to proceed with connection.
1181    let bss_description = match result.bss_description {
1182        Some(ref bss_description) => bss_description,
1183        None => {
1184            return Err(format_err!("RoamResult is missing BSS description from FIDL"));
1185        }
1186    };
1187    let ap_state = types::ApState::from(
1188        BssDescription::try_from(*bss_description.clone()).map_err(|error| {
1189            // This only occurs if an invalid `BssDescription` is received from SME, which should
1190            // never happen.
1191            format_err!("Failed to convert BSS description from FIDL: {:?}", error,)
1192        })?,
1193    );
1194    *options.ap_state = ap_state;
1195    options.bss_connect_start_time = fasync::MonotonicInstant::now();
1196    options.tracked_signals = TrackedSignals::new();
1197    options.initial_signal = options.ap_state.tracked.signal;
1198    options.tracked_signals.add(types::TimestampedSignal {
1199        time: fasync::MonotonicInstant::now(),
1200        signal: options.initial_signal,
1201    });
1202    options.post_connect_metric_timer =
1203        Box::pin(fasync::Timer::new(AVERAGE_SCORE_DELTA_MINIMUM_DURATION.after_now()));
1204    options.bss_connect_duration_metric_timer =
1205        Box::pin(fasync::Timer::new(METRICS_SHORT_CONNECT_DURATION.after_now()));
1206    // Re-initialize roam monitor for new BSS
1207    let (sender, roam_receiver) = mpsc::channel(ROAMING_CHANNEL_BUFFER_SIZE);
1208    options.roam_request_receiver = roam_receiver;
1209    options.roam_monitor_sender = common_options.roam_manager.initialize_roam_monitor(
1210        (*options.ap_state).clone(),
1211        options.network_identifier.clone(),
1212        options.credential.clone(),
1213        sender,
1214    );
1215    Ok(())
1216}
1217
1218#[allow(clippy::result_unit_err, reason = "mass allow for https://fxbug.dev/381896734")]
1219/// Get the disconnect reason corresponding to the connect reason. Return an error if the connect
1220/// reason does not correspond to a manual connect.
1221pub fn convert_manual_connect_to_disconnect_reason(
1222    reason: &types::ConnectReason,
1223) -> Result<types::DisconnectReason, ()> {
1224    match reason {
1225        types::ConnectReason::FidlConnectRequest => Ok(types::DisconnectReason::FidlConnectRequest),
1226        types::ConnectReason::ProactiveNetworkSwitch => {
1227            Ok(types::DisconnectReason::ProactiveNetworkSwitch)
1228        }
1229        types::ConnectReason::RetryAfterDisconnectDetected
1230        | types::ConnectReason::RetryAfterFailedConnectAttempt
1231        | types::ConnectReason::RegulatoryChangeReconnect
1232        | types::ConnectReason::IdleInterfaceAutoconnect
1233        | types::ConnectReason::NewSavedNetworkAutoconnect => Err(()),
1234    }
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239    use super::*;
1240    use crate::client::roaming::lib::{PolicyRoamRequest, RoamTriggerData};
1241    use crate::client::roaming::local_roam_manager::RoamServiceRequest;
1242    use crate::config_management::{network_config, new_past_connection_list};
1243    use crate::util::listener;
1244    use crate::util::state_machine::{StateMachineStatusReader, status_publisher_and_reader};
1245    use crate::util::testing::{
1246        ConnectResultRecord, ConnectionRecord, FakeSavedNetworksManager,
1247        generate_connect_selection, generate_disconnect_info, generate_policy_roam_request,
1248        generate_random_scanned_candidate, poll_sme_req, random_connection_data,
1249    };
1250    use assert_matches::assert_matches;
1251    use fidl::endpoints::{create_proxy, create_proxy_and_stream};
1252    use fidl::prelude::*;
1253    use fidl_fuchsia_wlan_policy as fidl_policy;
1254    use futures::Future;
1255    use futures::task::Poll;
1256    use ieee80211::MacAddrBytes;
1257    use std::pin::pin;
1258    use wlan_common::random_fidl_bss_description;
1259    use wlan_metrics_registry::PolicyDisconnectionMigratedMetricDimensionReason;
1260
1261    struct TestValues {
1262        common_options: CommonStateOptions,
1263        sme_req_stream: fidl_sme::ClientSmeRequestStream,
1264        saved_networks_manager: Arc<FakeSavedNetworksManager>,
1265        client_req_sender: mpsc::Sender<ManualRequest>,
1266        update_receiver: mpsc::UnboundedReceiver<listener::ClientListenerMessage>,
1267        telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
1268        defect_receiver: mpsc::Receiver<Defect>,
1269        roam_service_request_receiver: mpsc::Receiver<RoamServiceRequest>,
1270        status_reader: StateMachineStatusReader<Status>,
1271    }
1272
1273    fn test_setup() -> TestValues {
1274        let (client_req_sender, client_req_stream) = mpsc::channel(1);
1275        let (update_sender, update_receiver) = mpsc::unbounded();
1276        let (sme_proxy, sme_server) = create_proxy::<fidl_sme::ClientSmeMarker>();
1277        let sme_req_stream = sme_server.into_stream();
1278        let saved_networks = FakeSavedNetworksManager::new();
1279        let saved_networks_manager = Arc::new(saved_networks);
1280        let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1281        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1282        let (defect_sender, defect_receiver) = mpsc::channel(100);
1283        let (roam_service_request_sender, roam_service_request_receiver) = mpsc::channel(100);
1284        let roam_manager = RoamManager::new(roam_service_request_sender);
1285        let (status_publisher, status_reader) = status_publisher_and_reader::<Status>();
1286
1287        TestValues {
1288            common_options: CommonStateOptions {
1289                proxy: SmeForClientStateMachine::new(sme_proxy, 0, defect_sender.clone()),
1290                req_stream: client_req_stream.fuse(),
1291                update_sender,
1292                saved_networks_manager: saved_networks_manager.clone(),
1293                telemetry_sender,
1294                iface_id: 1,
1295                defect_sender,
1296                roam_manager,
1297                status_publisher,
1298            },
1299            sme_req_stream,
1300            saved_networks_manager,
1301            client_req_sender,
1302            update_receiver,
1303            telemetry_receiver,
1304            defect_receiver,
1305            roam_service_request_receiver,
1306            status_reader,
1307        }
1308    }
1309
1310    #[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
1311    async fn run_state_machine(fut: impl Future<Output = Result<State, ExitReason>> + 'static) {
1312        let state_machine = fut.into_state_machine();
1313        select! {
1314            _state_machine = state_machine.fuse() => return,
1315        }
1316    }
1317
1318    #[fuchsia::test]
1319    fn connecting_state_successfully_connects() {
1320        let mut exec = fasync::TestExecutor::new();
1321        let mut test_values = test_setup();
1322
1323        let connect_selection = generate_connect_selection();
1324        let bss_description =
1325            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1326
1327        // Store the network in the saved_networks_manager, so we can record connection success
1328        let save_fut = test_values.saved_networks_manager.store(
1329            connect_selection.target.network.clone(),
1330            connect_selection.target.credential.clone(),
1331        );
1332        let mut save_fut = pin!(save_fut);
1333        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
1334
1335        // Check that the saved networks manager has the expected initial data
1336        let saved_network = exec
1337            .run_singlethreaded(
1338                test_values
1339                    .saved_networks_manager
1340                    .lookup(&connect_selection.target.network.clone()),
1341            )
1342            .expect("failed to lookup network");
1343        assert!(!saved_network.has_ever_connected);
1344        assert!(saved_network.hidden_probability > 0.0);
1345
1346        let connecting_options =
1347            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1348        let initial_state = connecting_state(test_values.common_options, connecting_options);
1349        let fut = run_state_machine(initial_state);
1350        let mut fut = pin!(fut);
1351        let sme_fut = test_values.sme_req_stream.into_future();
1352        let mut sme_fut = pin!(sme_fut);
1353
1354        // Run the state machine
1355        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1356
1357        // Ensure a connect request is sent to the SME
1358        let connect_txn_handle = assert_matches!(
1359            poll_sme_req(&mut exec, &mut sme_fut),
1360            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1361                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1362                assert_eq!(req.bss_description, bss_description);
1363                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1364                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1365                // Send connection response.
1366                let (_stream, ctrl) = txn.expect("connect txn unused")
1367                    .into_stream_and_control_handle();
1368                ctrl
1369            }
1370        );
1371        connect_txn_handle
1372            .send_on_connect_result(&fake_successful_connect_result())
1373            .expect("failed to send connection completion");
1374
1375        // Check for a connecting update
1376        let client_state_update = ClientStateUpdate {
1377            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1378            networks: vec![ClientNetworkState {
1379                id: connect_selection.target.network.clone(),
1380                state: fidl_policy::ConnectionState::Connecting,
1381                status: None,
1382            }],
1383        };
1384        assert_matches!(
1385            test_values.update_receiver.try_recv(),
1386            Ok(listener::Message::NotifyListeners(updates)) => {
1387            assert_eq!(updates, client_state_update);
1388        });
1389
1390        // Progress the state machine
1391        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1392
1393        // Check for a connect update
1394        let client_state_update = ClientStateUpdate {
1395            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1396            networks: vec![ClientNetworkState {
1397                id: connect_selection.target.network.clone(),
1398                state: fidl_policy::ConnectionState::Connected,
1399                status: None,
1400            }],
1401        };
1402        assert_matches!(
1403            test_values.update_receiver.try_recv(),
1404            Ok(listener::Message::NotifyListeners(updates)) => {
1405            assert_eq!(updates, client_state_update);
1406        });
1407
1408        // Check that the connection was recorded to SavedNetworksManager
1409        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1410            let expected_connect_result = ConnectResultRecord {
1411                 id: connect_selection.target.network.clone(),
1412                 credential: connect_selection.target.credential.clone(),
1413                 bssid: types::Bssid::from(bss_description.bssid),
1414                 connect_result: fake_successful_connect_result(),
1415                 scan_type: connect_selection.target.bss.observation,
1416            };
1417            assert_eq!(data, &expected_connect_result);
1418        });
1419
1420        // Progress the state machine
1421        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1422
1423        // Ensure no further updates were sent to listeners
1424        assert_matches!(
1425            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1426            Poll::Pending
1427        );
1428    }
1429
1430    #[fuchsia::test]
1431    fn connecting_state_times_out() {
1432        let mut exec = fasync::TestExecutor::new();
1433        let mut test_values = test_setup();
1434
1435        let connect_selection = generate_connect_selection();
1436        let bss_description =
1437            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1438
1439        // Store the network in the saved_networks_manager
1440        let save_fut = test_values.saved_networks_manager.store(
1441            connect_selection.target.network.clone(),
1442            connect_selection.target.credential.clone(),
1443        );
1444        let mut save_fut = pin!(save_fut);
1445        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
1446
1447        // Prepare state machine
1448        let connecting_options =
1449            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1450        let initial_state = connecting_state(test_values.common_options, connecting_options);
1451        let fut = run_state_machine(initial_state);
1452        let mut fut = pin!(fut);
1453        let sme_fut = test_values.sme_req_stream.into_future();
1454        let mut sme_fut = pin!(sme_fut);
1455
1456        // Run the state machine
1457        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1458
1459        // Ensure a connect request is sent to the SME
1460        let connect_txn_handle = assert_matches!(
1461            poll_sme_req(&mut exec, &mut sme_fut),
1462            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1463                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1464                assert_eq!(req.bss_description, bss_description);
1465                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1466                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1467                let (_stream, ctrl) = txn.expect("connect txn unused")
1468                    .into_stream_and_control_handle();
1469                ctrl
1470            }
1471        );
1472
1473        // Check for a connecting update
1474        let client_state_update = ClientStateUpdate {
1475            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1476            networks: vec![ClientNetworkState {
1477                id: connect_selection.target.network.clone(),
1478                state: fidl_policy::ConnectionState::Connecting,
1479                status: None,
1480            }],
1481        };
1482        assert_matches!(
1483            test_values.update_receiver.try_recv(),
1484            Ok(listener::Message::NotifyListeners(updates)) => {
1485            assert_eq!(updates, client_state_update);
1486        });
1487
1488        // Respond with a SignalReport, which should not unblock connecting_state
1489        connect_txn_handle
1490            .send_on_signal_report(&fidl_internal::SignalReportIndication {
1491                rssi_dbm: -25,
1492                snr_db: 30,
1493                tx_rate_500kbps: 0,
1494            })
1495            .expect("failed to send singal report");
1496
1497        // Run the state machine. Should still be pending
1498        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1499
1500        // Wake up the next timer, which is the timeout for the connect request.
1501        assert!(exec.wake_next_timer().is_some());
1502
1503        // State machine should exit.
1504        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1505    }
1506
1507    #[fuchsia::test]
1508    fn connecting_state_successfully_scans_and_connects() {
1509        let mut exec = fasync::TestExecutor::new_with_fake_time();
1510        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(123));
1511        let mut test_values = test_setup();
1512
1513        let connect_selection = generate_connect_selection();
1514        let bss_description =
1515            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1516
1517        // Set how the SavedNetworksManager should respond to lookup_compatible for the scan.
1518        let expected_config = network_config::NetworkConfig::new(
1519            connect_selection.target.network.clone(),
1520            connect_selection.target.credential.clone(),
1521            connect_selection.target.saved_network_info.has_ever_connected,
1522            None,
1523        )
1524        .expect("failed to create network config");
1525        test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
1526
1527        let connecting_options =
1528            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1529        let initial_state = connecting_state(test_values.common_options, connecting_options);
1530        let fut = run_state_machine(initial_state);
1531        let mut fut = pin!(fut);
1532
1533        // Run the state machine
1534        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1535
1536        // Ensure a connect request is sent to the SME
1537        let sme_fut = test_values.sme_req_stream.into_future();
1538        let mut sme_fut = pin!(sme_fut);
1539        let time_to_connect = zx::MonotonicDuration::from_seconds(30);
1540        let connect_txn_handle = assert_matches!(
1541            poll_sme_req(&mut exec, &mut sme_fut),
1542            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1543                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1544                assert_eq!(req.bss_description, bss_description.clone());
1545                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1546                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1547                // Send connection response.
1548                exec.set_fake_time(fasync::MonotonicInstant::after(time_to_connect));
1549                let (_stream, ctrl) = txn.expect("connect txn unused")
1550                    .into_stream_and_control_handle();
1551                ctrl
1552            }
1553        );
1554        connect_txn_handle
1555            .send_on_connect_result(&fake_successful_connect_result())
1556            .expect("failed to send connection completion");
1557
1558        // Check for a connecting update
1559        let client_state_update = ClientStateUpdate {
1560            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1561            networks: vec![ClientNetworkState {
1562                id: connect_selection.target.network.clone(),
1563                state: fidl_policy::ConnectionState::Connecting,
1564                status: None,
1565            }],
1566        };
1567        assert_matches!(
1568            test_values.update_receiver.try_recv(),
1569            Ok(listener::Message::NotifyListeners(updates)) => {
1570            assert_eq!(updates, client_state_update);
1571        });
1572
1573        // Progress the state machine
1574        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1575
1576        // Check for a connect update
1577        let client_state_update = ClientStateUpdate {
1578            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1579            networks: vec![ClientNetworkState {
1580                id: connect_selection.target.network.clone(),
1581                state: fidl_policy::ConnectionState::Connected,
1582                status: None,
1583            }],
1584        };
1585        assert_matches!(
1586            test_values.update_receiver.try_recv(),
1587            Ok(listener::Message::NotifyListeners(updates)) => {
1588            assert_eq!(updates, client_state_update);
1589        });
1590
1591        // Check that the saved networks manager has the connection result recorded
1592        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1593            let expected_connect_result = ConnectResultRecord {
1594                 id: connect_selection.target.network.clone(),
1595                 credential: connect_selection.target.credential.clone(),
1596                 bssid: types::Bssid::from(bss_description.bssid),
1597                 connect_result: fake_successful_connect_result(),
1598                 scan_type: connect_selection.target.bss.observation,
1599            };
1600            assert_eq!(data, &expected_connect_result);
1601        });
1602
1603        // Check that connected telemetry event is sent
1604        assert_matches!(
1605            test_values.telemetry_receiver.try_recv(),
1606            Ok(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ }) => {
1607                assert_eq!(bss_description, ap_state.original().clone().into());
1608                assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1609                assert_eq!(policy_connect_reason, Some(connect_selection.reason));
1610                assert_eq!(result, fake_successful_connect_result());
1611            }
1612        );
1613
1614        // Progress the state machine
1615        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1616
1617        // Ensure no further updates were sent to listeners
1618        assert_matches!(
1619            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1620            Poll::Pending
1621        );
1622
1623        // Verify the Connected status was set.
1624        let status = test_values.status_reader.read_status().expect("failed to read status");
1625        assert_matches!(status, Status::Connected { .. });
1626
1627        // Send a disconnect and check that the connection data is correctly recorded
1628        let is_sme_reconnecting = false;
1629        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
1630        connect_txn_handle
1631            .send_on_disconnect(&fidl_disconnect_info)
1632            .expect("failed to send disconnection event");
1633        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1634
1635        // Verify roam monitor request was sent.
1636        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
1637            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
1638        });
1639
1640        // Run the state machine
1641        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1642
1643        let expected_recorded_connection = ConnectionRecord {
1644            id: connect_selection.target.network.clone(),
1645            credential: connect_selection.target.credential.clone(),
1646            data: PastConnectionData {
1647                bssid: types::Bssid::from(bss_description.bssid),
1648                disconnect_time: fasync::MonotonicInstant::now(),
1649                connection_uptime: zx::MonotonicDuration::from_minutes(0),
1650                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
1651                signal_at_disconnect: types::Signal {
1652                    rssi_dbm: bss_description.rssi_dbm,
1653                    snr_db: bss_description.snr_db,
1654                },
1655                // TODO: record average phy rate over connection once available
1656                average_tx_rate: 0,
1657            },
1658        };
1659        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [data] => {
1660            assert_eq!(data, &expected_recorded_connection);
1661        });
1662    }
1663
1664    #[fuchsia::test]
1665    fn connecting_state_fails_to_connect_and_retries() {
1666        let mut exec = fasync::TestExecutor::new();
1667        let mut test_values = test_setup();
1668
1669        let connect_selection = generate_connect_selection();
1670        let bss_description =
1671            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1672
1673        let connecting_options =
1674            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1675        let initial_state = connecting_state(test_values.common_options, connecting_options);
1676        let fut = run_state_machine(initial_state);
1677        let mut fut = pin!(fut);
1678        let sme_fut = test_values.sme_req_stream.into_future();
1679        let mut sme_fut = pin!(sme_fut);
1680
1681        // Run the state machine
1682        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1683
1684        // Ensure a connect request is sent to the SME
1685        let mut connect_txn_handle = assert_matches!(
1686            poll_sme_req(&mut exec, &mut sme_fut),
1687            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1688                assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec());
1689                 // Send connection response.
1690                let (_stream, ctrl) = txn.expect("connect txn unused")
1691                    .into_stream_and_control_handle();
1692                ctrl
1693            }
1694        );
1695        let connect_result = fidl_sme::ConnectResult {
1696            code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1697            ..fake_successful_connect_result()
1698        };
1699        connect_txn_handle
1700            .send_on_connect_result(&connect_result)
1701            .expect("failed to send connection completion");
1702
1703        // Check for a connecting update
1704        let client_state_update = ClientStateUpdate {
1705            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1706            networks: vec![ClientNetworkState {
1707                id: types::NetworkIdentifier {
1708                    ssid: connect_selection.target.network.ssid.clone(),
1709                    security_type: types::SecurityType::Wpa2,
1710                },
1711                state: fidl_policy::ConnectionState::Connecting,
1712                status: None,
1713            }],
1714        };
1715        assert_matches!(
1716            test_values.update_receiver.try_recv(),
1717            Ok(listener::Message::NotifyListeners(updates)) => {
1718            assert_eq!(updates, client_state_update);
1719        });
1720
1721        // Progress the state machine
1722        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1723        assert!(exec.wake_next_timer().is_some());
1724        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1725
1726        // Check that connect result telemetry event is sent
1727        assert_matches!(
1728            test_values.telemetry_receiver.try_recv(),
1729            Ok(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ }) => {
1730                assert_eq!(bss_description, ap_state.original().clone().into());
1731                assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1732                assert_eq!(policy_connect_reason, Some(connect_selection.reason));
1733                assert_eq!(result, connect_result);
1734            }
1735        );
1736
1737        // Ensure a disconnect request is sent to the SME
1738        assert_matches!(
1739            poll_sme_req(&mut exec, &mut sme_fut),
1740            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FailedToConnect }) => {
1741                responder.send().expect("could not send sme response");
1742            }
1743        );
1744
1745        // Progress the state machine
1746        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1747
1748        // Ensure a connect request is sent to the SME
1749        connect_txn_handle = assert_matches!(
1750            poll_sme_req(&mut exec, &mut sme_fut),
1751            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1752                assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec());
1753                assert_eq!(req.bss_description, Sequestered::release(connect_selection.target.bss.bss_description));
1754                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1755                 // Send connection response.
1756                let (_stream, ctrl) = txn.expect("connect txn unused")
1757                    .into_stream_and_control_handle();
1758                ctrl
1759            }
1760        );
1761        let connect_result = fake_successful_connect_result();
1762        connect_txn_handle
1763            .send_on_connect_result(&connect_result)
1764            .expect("failed to send connection completion");
1765
1766        // Progress the state machine
1767        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1768
1769        // Empty update sent to NotifyListeners (which in this case, will not actually be sent.)
1770        assert_matches!(
1771            test_values.update_receiver.try_recv(),
1772            Ok(listener::Message::NotifyListeners(ClientStateUpdate {
1773                state: fidl_policy::WlanClientState::ConnectionsEnabled,
1774                networks
1775            })) => {
1776                assert!(networks.is_empty());
1777            }
1778        );
1779
1780        // A defect should be logged.
1781        assert_matches!(
1782            test_values.defect_receiver.try_recv(),
1783            Ok(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))
1784        );
1785
1786        // Check for a connected update
1787        let client_state_update = ClientStateUpdate {
1788            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1789            networks: vec![ClientNetworkState {
1790                id: types::NetworkIdentifier {
1791                    ssid: connect_selection.target.network.ssid.clone(),
1792                    security_type: types::SecurityType::Wpa2,
1793                },
1794                state: fidl_policy::ConnectionState::Connected,
1795                status: None,
1796            }],
1797        };
1798        assert_matches!(
1799            test_values.update_receiver.try_recv(),
1800            Ok(listener::Message::NotifyListeners(updates)) => {
1801            assert_eq!(updates, client_state_update);
1802        });
1803
1804        // Progress the state machine
1805        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1806
1807        // Ensure no further updates were sent to listeners
1808        assert_matches!(
1809            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1810            Poll::Pending
1811        );
1812    }
1813
1814    #[fuchsia::test]
1815    fn connecting_state_fails_to_connect_at_max_retries() {
1816        let mut exec = fasync::TestExecutor::new();
1817        let mut test_values = test_setup();
1818
1819        let connect_selection = generate_connect_selection();
1820        let bss_description =
1821            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1822
1823        // save network to check that failed connect is recorded
1824        assert!(
1825            exec.run_singlethreaded(test_values.saved_networks_manager.store(
1826                connect_selection.target.network.clone(),
1827                connect_selection.target.credential.clone()
1828            ),)
1829                .expect("Failed to save network")
1830                .is_none()
1831        );
1832
1833        let connecting_options = ConnectingOptions {
1834            connect_selection: connect_selection.clone(),
1835            attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
1836        };
1837        let initial_state = connecting_state(test_values.common_options, connecting_options);
1838        let fut = run_state_machine(initial_state);
1839        let mut fut = pin!(fut);
1840        let sme_fut = test_values.sme_req_stream.into_future();
1841        let mut sme_fut = pin!(sme_fut);
1842
1843        // Run the state machine
1844        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1845
1846        // Ensure a connect request is sent to the SME
1847        assert_matches!(
1848            poll_sme_req(&mut exec, &mut sme_fut),
1849            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1850                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1851                assert_eq!(req.bss_description, bss_description.clone());
1852                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1853                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1854                 // Send connection response.
1855                let (_stream, ctrl) = txn.expect("connect txn unused")
1856                    .into_stream_and_control_handle();
1857                let connect_result = fidl_sme::ConnectResult {
1858                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1859                    ..fake_successful_connect_result()
1860                };
1861                ctrl
1862                    .send_on_connect_result(&connect_result)
1863                    .expect("failed to send connection completion");
1864            }
1865        );
1866
1867        // After failing to reconnect, the state machine should exit so that the state machine
1868        // monitor can attempt to reconnect the interface.
1869        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1870
1871        // Check for a connect update
1872        let client_state_update = ClientStateUpdate {
1873            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1874            networks: vec![ClientNetworkState {
1875                id: connect_selection.target.network.clone(),
1876                state: fidl_policy::ConnectionState::Failed,
1877                status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
1878            }],
1879        };
1880        assert_matches!(
1881            test_values.update_receiver.try_recv(),
1882            Ok(listener::Message::NotifyListeners(updates)) => {
1883            assert_eq!(updates, client_state_update);
1884        });
1885
1886        // Check that failure was recorded in SavedNetworksManager
1887        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1888            let connect_result = fidl_sme::ConnectResult {
1889                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1890                is_credential_rejected: false,
1891                is_reconnect: false,
1892            };
1893            let expected_connect_result = ConnectResultRecord {
1894                 id: connect_selection.target.network.clone(),
1895                 credential: connect_selection.target.credential.clone(),
1896                 bssid: types::Bssid::from(bss_description.bssid),
1897                 connect_result,
1898                 scan_type: connect_selection.target.bss.observation,
1899            };
1900            assert_eq!(data, &expected_connect_result);
1901        });
1902
1903        // A defect should be logged.
1904        assert_matches!(
1905            test_values.defect_receiver.try_recv(),
1906            Ok(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))
1907        );
1908    }
1909
1910    #[fuchsia::test]
1911    fn connecting_state_fails_to_connect_with_bad_credentials() {
1912        let mut exec = fasync::TestExecutor::new();
1913        let mut test_values = test_setup();
1914
1915        let connect_selection = generate_connect_selection();
1916        let bss_description =
1917            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1918
1919        assert!(
1920            exec.run_singlethreaded(test_values.saved_networks_manager.store(
1921                connect_selection.target.network.clone(),
1922                connect_selection.target.credential.clone()
1923            ),)
1924                .expect("Failed to save network")
1925                .is_none()
1926        );
1927
1928        let connecting_options = ConnectingOptions {
1929            connect_selection: connect_selection.clone(),
1930            attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
1931        };
1932        let initial_state = connecting_state(test_values.common_options, connecting_options);
1933        let fut = run_state_machine(initial_state);
1934        let mut fut = pin!(fut);
1935        let sme_fut = test_values.sme_req_stream.into_future();
1936        let mut sme_fut = pin!(sme_fut);
1937
1938        // Run the state machine
1939        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1940
1941        // Ensure a connect request is sent to the SME
1942        assert_matches!(
1943            poll_sme_req(&mut exec, &mut sme_fut),
1944            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1945                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1946                assert_eq!(req.bss_description, bss_description.clone());
1947                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1948                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1949                 // Send connection response.
1950                let (_stream, ctrl) = txn.expect("connect txn unused")
1951                    .into_stream_and_control_handle();
1952                let connect_result = fidl_sme::ConnectResult {
1953                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1954                    is_credential_rejected: true,
1955                    ..fake_successful_connect_result()
1956                };
1957                ctrl
1958                    .send_on_connect_result(&connect_result)
1959                    .expect("failed to send connection completion");
1960            }
1961        );
1962
1963        // The state machine should exit when bad credentials are detected so that the state
1964        // machine monitor can try to connect to another network.
1965        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1966
1967        // Check for a connect update
1968        let client_state_update = ClientStateUpdate {
1969            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1970            networks: vec![ClientNetworkState {
1971                id: connect_selection.target.network.clone(),
1972                state: fidl_policy::ConnectionState::Failed,
1973                status: Some(fidl_policy::DisconnectStatus::CredentialsFailed),
1974            }],
1975        };
1976        assert_matches!(
1977            test_values.update_receiver.try_recv(),
1978            Ok(listener::Message::NotifyListeners(updates)) => {
1979            assert_eq!(updates, client_state_update);
1980        });
1981
1982        // Check that failure was recorded to SavedNetworksManager
1983        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1984            let connect_result = fidl_sme::ConnectResult {
1985                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1986                is_credential_rejected: true,
1987                is_reconnect: false,
1988            };
1989            let expected_connect_result = ConnectResultRecord {
1990                 id: connect_selection.target.network.clone(),
1991                 credential: connect_selection.target.credential.clone(),
1992                 bssid: types::Bssid::from(bss_description.bssid),
1993                 connect_result,
1994                 scan_type: connect_selection.target.bss.observation,
1995            };
1996            assert_eq!(data, &expected_connect_result);
1997        });
1998
1999        // No defect should have been observed.
2000        assert_matches!(test_values.defect_receiver.try_recv(), Err(e) if e.is_closed());
2001    }
2002
2003    #[fuchsia::test]
2004    fn connecting_state_gets_duplicate_connect_selection() {
2005        let mut exec = fasync::TestExecutor::new();
2006        let mut test_values = test_setup();
2007
2008        let connect_selection = generate_connect_selection();
2009        let bss_description =
2010            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2011
2012        let connecting_options =
2013            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2014        let initial_state = connecting_state(test_values.common_options, connecting_options);
2015        let fut = run_state_machine(initial_state);
2016        let mut fut = pin!(fut);
2017        let sme_fut = test_values.sme_req_stream.into_future();
2018        let mut sme_fut = pin!(sme_fut);
2019
2020        // Run the state machine
2021        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2022
2023        // Check for a connecting update
2024        let client_state_update = ClientStateUpdate {
2025            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2026            networks: vec![ClientNetworkState {
2027                id: types::NetworkIdentifier {
2028                    ssid: connect_selection.target.network.ssid.clone(),
2029                    security_type: types::SecurityType::Wpa2,
2030                },
2031                state: fidl_policy::ConnectionState::Connecting,
2032                status: None,
2033            }],
2034        };
2035        assert_matches!(
2036            test_values.update_receiver.try_recv(),
2037            Ok(listener::Message::NotifyListeners(updates)) => {
2038            assert_eq!(updates, client_state_update);
2039        });
2040
2041        // Send a duplicate connect request
2042        let mut client = Client::new(test_values.client_req_sender);
2043        let duplicate_request = types::ConnectSelection {
2044            // this incoming request should be deduped regardless of the reason
2045            reason: types::ConnectReason::ProactiveNetworkSwitch,
2046            ..connect_selection.clone()
2047        };
2048        client.connect(duplicate_request).expect("failed to make request");
2049
2050        // Progress the state machine
2051        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2052
2053        // Ensure a connect request is sent to the SME
2054        let connect_txn_handle = assert_matches!(
2055            poll_sme_req(&mut exec, &mut sme_fut),
2056            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
2057                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
2058                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
2059                assert_eq!(req.bss_description, bss_description);
2060                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
2061                 // Send connection response.
2062                let (_stream, ctrl) = txn.expect("connect txn unused")
2063                    .into_stream_and_control_handle();
2064                ctrl
2065            }
2066        );
2067        connect_txn_handle
2068            .send_on_connect_result(&fake_successful_connect_result())
2069            .expect("failed to send connection completion");
2070
2071        // Progress the state machine
2072        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2073
2074        // Check for a connect update
2075        let client_state_update = ClientStateUpdate {
2076            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2077            networks: vec![ClientNetworkState {
2078                id: connect_selection.target.network.clone(),
2079                state: fidl_policy::ConnectionState::Connected,
2080                status: None,
2081            }],
2082        };
2083        assert_matches!(
2084            test_values.update_receiver.try_recv(),
2085            Ok(listener::Message::NotifyListeners(updates)) => {
2086            assert_eq!(updates, client_state_update);
2087        });
2088
2089        // Progress the state machine
2090        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2091
2092        // Ensure no further updates were sent to listeners
2093        assert_matches!(
2094            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
2095            Poll::Pending
2096        );
2097    }
2098
2099    #[fuchsia::test]
2100    fn connecting_state_has_broken_sme() {
2101        let mut exec = fasync::TestExecutor::new();
2102        let test_values = test_setup();
2103
2104        let connect_selection = generate_connect_selection();
2105
2106        let connecting_options =
2107            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2108        let initial_state = connecting_state(test_values.common_options, connecting_options);
2109        let fut = run_state_machine(initial_state);
2110        let mut fut = pin!(fut);
2111
2112        // Break the SME by dropping the server end of the SME stream, so it causes an error
2113        drop(test_values.sme_req_stream);
2114
2115        // Ensure the state machine exits
2116        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2117    }
2118
2119    #[fuchsia::test]
2120    fn connected_state_gets_disconnect_request() {
2121        let mut exec = fasync::TestExecutor::new_with_fake_time();
2122        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2123
2124        let mut test_values = test_setup();
2125        let mut telemetry_receiver = test_values.telemetry_receiver;
2126        let connect_selection = generate_connect_selection();
2127        let bss_description =
2128            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2129        let init_ap_state =
2130            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2131
2132        let (connect_txn_proxy, _connect_txn_stream) =
2133            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2134        let options = ConnectedOptions::new(
2135            &mut test_values.common_options,
2136            Box::new(init_ap_state.clone()),
2137            connect_selection.target.network_has_multiple_bss,
2138            connect_selection.target.network.clone(),
2139            connect_selection.target.credential.clone(),
2140            connect_selection.reason,
2141            connect_txn_proxy.take_event_stream(),
2142            false,
2143        );
2144        let initial_state = connected_state(test_values.common_options, options);
2145        let fut = run_state_machine(initial_state);
2146        let mut fut = pin!(fut);
2147        let sme_fut = test_values.sme_req_stream.into_future();
2148        let mut sme_fut = pin!(sme_fut);
2149
2150        let disconnect_time =
2151            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2152
2153        // Run the state machine
2154        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2155
2156        // Verify roam monitor request was sent.
2157        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2158            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2159        });
2160
2161        // Run the state machine
2162        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2163
2164        // Run forward to get post connection signals metrics
2165        exec.set_fake_time(fasync::MonotonicInstant::after(
2166            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2167        ));
2168        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2169        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2170            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2171        });
2172
2173        // Run forward to get long duration signals metrics
2174        exec.set_fake_time(fasync::MonotonicInstant::after(
2175            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2176        ));
2177        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2178        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2179            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2180        });
2181
2182        // Run forward to disconnect time
2183        exec.set_fake_time(disconnect_time);
2184        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2185
2186        // Send a disconnect request
2187        let mut client = Client::new(test_values.client_req_sender);
2188        let (sender, mut receiver) = oneshot::channel();
2189        client
2190            .disconnect(types::DisconnectReason::FidlStopClientConnectionsRequest, sender)
2191            .expect("failed to make request");
2192
2193        // Run the state machine
2194        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2195
2196        // Respond to the SME disconnect
2197        assert_matches!(
2198            poll_sme_req(&mut exec, &mut sme_fut),
2199            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest }) => {
2200                responder.send().expect("could not send sme response");
2201            }
2202        );
2203
2204        // Once the disconnect is processed, the state machine should exit.
2205        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2206
2207        // Check for a disconnect update and the responder
2208        let client_state_update = ClientStateUpdate {
2209            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2210            networks: vec![ClientNetworkState {
2211                id: connect_selection.target.network.clone(),
2212                state: fidl_policy::ConnectionState::Disconnected,
2213                status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
2214            }],
2215        };
2216        assert_matches!(
2217            test_values.update_receiver.try_recv(),
2218            Ok(listener::Message::NotifyListeners(updates)) => {
2219            assert_eq!(updates, client_state_update);
2220        });
2221        assert_matches!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
2222
2223        // Disconnect telemetry event sent
2224        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2225            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2226                assert!(!track_subsequent_downtime);
2227                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2228                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2229                    assert!(!is_sme_reconnecting);
2230                    assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest));
2231                    assert_eq!(previous_connect_reason, connect_selection.reason);
2232                    assert_eq!(ap_state, init_ap_state.clone());
2233                });
2234            });
2235        });
2236
2237        // The disconnect should have been recorded for the saved network config.
2238        let expected_recorded_connection = ConnectionRecord {
2239            id: connect_selection.target.network.clone(),
2240            credential: connect_selection.target.credential.clone(),
2241            data: PastConnectionData {
2242                bssid: init_ap_state.original().bssid,
2243                disconnect_time,
2244                connection_uptime: zx::MonotonicDuration::from_hours(12),
2245                disconnect_reason: types::DisconnectReason::FidlStopClientConnectionsRequest,
2246                signal_at_disconnect: types::Signal {
2247                    rssi_dbm: bss_description.rssi_dbm,
2248                    snr_db: bss_description.snr_db,
2249                },
2250                // TODO: record average phy rate over connection once available
2251                average_tx_rate: 0,
2252            },
2253        };
2254        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2255            assert_eq!(connection_data, &expected_recorded_connection);
2256        });
2257    }
2258
2259    #[fuchsia::test]
2260    fn connected_state_records_unexpected_disconnect() {
2261        let mut exec = fasync::TestExecutor::new_with_fake_time();
2262        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2263
2264        let mut test_values = test_setup();
2265        let mut telemetry_receiver = test_values.telemetry_receiver;
2266
2267        let connect_selection = generate_connect_selection();
2268        let bss_description =
2269            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2270        let init_ap_state =
2271            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2272
2273        // Save the network in order to later record the disconnect to it.
2274        let save_fut = test_values.saved_networks_manager.store(
2275            connect_selection.target.network.clone(),
2276            connect_selection.target.credential.clone(),
2277        );
2278        let mut save_fut = pin!(save_fut);
2279        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
2280
2281        let (connect_txn_proxy, connect_txn_stream) =
2282            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2283        let connect_txn_handle = connect_txn_stream.control_handle();
2284        let options = ConnectedOptions::new(
2285            &mut test_values.common_options,
2286            Box::new(init_ap_state.clone()),
2287            connect_selection.target.network_has_multiple_bss,
2288            connect_selection.target.network.clone(),
2289            connect_selection.target.credential.clone(),
2290            connect_selection.reason,
2291            connect_txn_proxy.take_event_stream(),
2292            false,
2293        );
2294
2295        // Start the state machine in the connected state.
2296        let initial_state = connected_state(test_values.common_options, options);
2297        let fut = run_state_machine(initial_state);
2298        let mut fut = pin!(fut);
2299        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2300
2301        // Verify roam monitor request was sent.
2302        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2303            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2304        });
2305
2306        // Run the state machine
2307        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2308
2309        let disconnect_time =
2310            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2311        exec.set_fake_time(disconnect_time);
2312
2313        // SME notifies Policy of disconnection
2314        let fidl_disconnect_info = generate_disconnect_info(false);
2315        connect_txn_handle
2316            .send_on_disconnect(&fidl_disconnect_info)
2317            .expect("failed to send disconnection event");
2318        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2319
2320        // The disconnect should have been recorded for the saved network config.
2321        let expected_recorded_connection = ConnectionRecord {
2322            id: connect_selection.target.network.clone(),
2323            credential: connect_selection.target.credential.clone(),
2324            data: PastConnectionData {
2325                bssid: init_ap_state.original().bssid,
2326                disconnect_time,
2327                connection_uptime: zx::MonotonicDuration::from_hours(12),
2328                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
2329                signal_at_disconnect: types::Signal {
2330                    rssi_dbm: bss_description.rssi_dbm,
2331                    snr_db: bss_description.snr_db,
2332                },
2333                // TODO: record average phy rate over connection once available
2334                average_tx_rate: 0,
2335            },
2336        };
2337        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2338            assert_eq!(connection_data, &expected_recorded_connection);
2339        });
2340
2341        // Disconnect telemetry event sent
2342        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2343            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2344                assert!(track_subsequent_downtime);
2345                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2346                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2347                    assert!(!is_sme_reconnecting);
2348                    assert_eq!(disconnect_source, fidl_disconnect_info.disconnect_source);
2349                    assert_eq!(previous_connect_reason, connect_selection.reason);
2350                    assert_eq!(ap_state, init_ap_state);
2351                });
2352            });
2353        });
2354    }
2355
2356    #[fuchsia::test]
2357    fn connected_state_reconnect_resets_connected_duration() {
2358        let mut exec = fasync::TestExecutor::new_with_fake_time();
2359        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2360
2361        let mut test_values = test_setup();
2362        let mut telemetry_receiver = test_values.telemetry_receiver;
2363
2364        let connect_selection = generate_connect_selection();
2365        let bss_description =
2366            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2367        let ap_state =
2368            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2369
2370        let (connect_txn_proxy, connect_txn_stream) =
2371            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2372        let connect_txn_handle = connect_txn_stream.control_handle();
2373        let options = ConnectedOptions::new(
2374            &mut test_values.common_options,
2375            Box::new(ap_state.clone()),
2376            connect_selection.target.network_has_multiple_bss,
2377            connect_selection.target.network.clone(),
2378            connect_selection.target.credential.clone(),
2379            connect_selection.reason,
2380            connect_txn_proxy.take_event_stream(),
2381            false,
2382        );
2383        let initial_state = connected_state(test_values.common_options, options);
2384        let fut = run_state_machine(initial_state);
2385        let mut fut = pin!(fut);
2386
2387        let disconnect_time =
2388            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2389
2390        // Run the state machine
2391        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2392
2393        // Verify roam monitor request was sent.
2394        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2395            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2396        });
2397
2398        // Run the state machine
2399        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2400
2401        // Run forward to get post connection score metrics
2402        exec.set_fake_time(fasync::MonotonicInstant::after(
2403            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2404        ));
2405        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2406        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2407            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2408        });
2409
2410        // Run forward to get long duration signals metrics
2411        exec.set_fake_time(fasync::MonotonicInstant::after(
2412            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2413        ));
2414        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2415        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2416            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2417        });
2418
2419        // Run forward to disconnect time
2420        exec.set_fake_time(disconnect_time);
2421        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2422
2423        // SME notifies Policy of disconnection with SME-initiated reconnect
2424        let is_sme_reconnecting = true;
2425        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
2426        connect_txn_handle
2427            .send_on_disconnect(&fidl_disconnect_info)
2428            .expect("failed to send disconnection event");
2429        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2430
2431        // Disconnect telemetry event sent
2432        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2433            assert_matches!(event, TelemetryEvent::Disconnected { info: Some(info), .. } => {
2434                assert_eq!(info.connected_duration, zx::MonotonicDuration::from_hours(12));
2435            });
2436        });
2437
2438        // SME notifies Policy of reconnection successful
2439        exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1)));
2440        let connect_result =
2441            fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
2442        connect_txn_handle
2443            .send_on_connect_result(&connect_result)
2444            .expect("failed to send connect result event");
2445
2446        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2447        assert_matches!(telemetry_receiver.try_recv(), Ok(TelemetryEvent::ConnectResult { .. }));
2448
2449        // SME notifies Policy of another disconnection
2450        exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(2)));
2451        let is_sme_reconnecting = false;
2452        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
2453        connect_txn_handle
2454            .send_on_disconnect(&fidl_disconnect_info)
2455            .expect("failed to send disconnection event");
2456        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2457
2458        // Another disconnect telemetry event sent
2459        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2460            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
2461                assert_eq!(info.unwrap().connected_duration, zx::MonotonicDuration::from_hours(2));
2462            });
2463        });
2464    }
2465
2466    #[fuchsia::test]
2467    fn connected_state_records_unexpected_disconnect_unspecified_bss() {
2468        let mut exec = fasync::TestExecutor::new_with_fake_time();
2469        let connection_attempt_time = fasync::MonotonicInstant::from_nanos(0);
2470        exec.set_fake_time(connection_attempt_time);
2471        let mut test_values = test_setup();
2472
2473        let connect_selection = generate_connect_selection();
2474        let bss_description =
2475            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2476
2477        // Setup for network selection in the connecting state to select the intended network.
2478        let expected_config = network_config::NetworkConfig::new(
2479            connect_selection.target.network.clone(),
2480            connect_selection.target.credential.clone(),
2481            false,
2482            None,
2483        )
2484        .expect("failed to create network config");
2485        test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
2486
2487        let connecting_options =
2488            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2489        let initial_state = connecting_state(test_values.common_options, connecting_options);
2490        let state_fut = run_state_machine(initial_state);
2491        let mut state_fut = pin!(state_fut);
2492        let sme_fut = test_values.sme_req_stream.into_future();
2493        let mut sme_fut = pin!(sme_fut);
2494
2495        // Run the state machine
2496        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2497
2498        // Run the state machine
2499        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2500
2501        let time_to_connect = zx::MonotonicDuration::from_seconds(10);
2502        exec.set_fake_time(fasync::MonotonicInstant::after(time_to_connect));
2503
2504        // Process connect request sent to SME
2505        let connect_txn_handle = assert_matches!(
2506            poll_sme_req(&mut exec, &mut sme_fut),
2507            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => {
2508                 // Send connection response.
2509                let (_stream, ctrl) = txn.expect("connect txn unused")
2510                    .into_stream_and_control_handle();
2511                ctrl
2512            }
2513        );
2514        connect_txn_handle
2515            .send_on_connect_result(&fake_successful_connect_result())
2516            .expect("failed to send connection completion");
2517        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2518
2519        // SME notifies Policy of disconnection.
2520        let disconnect_time = fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(5));
2521        exec.set_fake_time(disconnect_time);
2522        let is_sme_reconnecting = false;
2523        connect_txn_handle
2524            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2525            .expect("failed to send disconnection event");
2526        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2527
2528        // Verify roam monitor request was sent.
2529        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2530            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2531        });
2532
2533        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2534        // The connection data should have been recorded at disconnect.
2535        let expected_recorded_connection = ConnectionRecord {
2536            id: connect_selection.target.network.clone(),
2537            credential: connect_selection.target.credential.clone(),
2538            data: PastConnectionData {
2539                bssid: types::Bssid::from(bss_description.bssid),
2540                disconnect_time,
2541                connection_uptime: zx::MonotonicDuration::from_hours(5),
2542                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
2543                signal_at_disconnect: types::Signal {
2544                    rssi_dbm: bss_description.rssi_dbm,
2545                    snr_db: bss_description.snr_db,
2546                },
2547                average_tx_rate: 0,
2548            },
2549        };
2550        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2551            assert_eq!(connection_data, &expected_recorded_connection);
2552        });
2553    }
2554
2555    #[fuchsia::test]
2556    fn connected_state_gets_duplicate_connect_selection() {
2557        let mut exec = fasync::TestExecutor::new_with_fake_time();
2558        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2559        let mut test_values = test_setup();
2560        let mut telemetry_receiver = test_values.telemetry_receiver;
2561
2562        let connect_selection = generate_connect_selection();
2563        let bss_description =
2564            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2565        let ap_state =
2566            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2567
2568        let (connect_txn_proxy, _connect_txn_stream) =
2569            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2570        let options = ConnectedOptions::new(
2571            &mut test_values.common_options,
2572            Box::new(ap_state.clone()),
2573            connect_selection.target.network_has_multiple_bss,
2574            connect_selection.target.network.clone(),
2575            connect_selection.target.credential.clone(),
2576            connect_selection.reason,
2577            connect_txn_proxy.take_event_stream(),
2578            false,
2579        );
2580        let initial_state = connected_state(test_values.common_options, options);
2581        let fut = run_state_machine(initial_state);
2582        let mut fut = pin!(fut);
2583        let sme_fut = test_values.sme_req_stream.into_future();
2584        let mut sme_fut = pin!(sme_fut);
2585
2586        // Send another duplicate request
2587        let mut client = Client::new(test_values.client_req_sender);
2588        client.connect(connect_selection.clone()).expect("failed to make request");
2589
2590        // Run the state machine
2591        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2592
2593        // Ensure nothing was sent to the SME
2594        assert_matches!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending);
2595
2596        // No telemetry event is sent
2597        assert_matches!(telemetry_receiver.try_recv(), Err(_));
2598    }
2599
2600    #[fuchsia::test]
2601    fn connected_state_gets_different_connect_selection() {
2602        let mut exec = fasync::TestExecutor::new_with_fake_time();
2603        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2604
2605        let mut test_values = test_setup();
2606        let mut telemetry_receiver = test_values.telemetry_receiver;
2607
2608        let first_connect_selection = generate_connect_selection();
2609        let first_bss_desc =
2610            Sequestered::release(first_connect_selection.target.bss.bss_description.clone());
2611        let first_ap_state =
2612            types::ApState::from(BssDescription::try_from(first_bss_desc.clone()).unwrap());
2613        let second_connect_selection = types::ConnectSelection {
2614            reason: types::ConnectReason::ProactiveNetworkSwitch,
2615            ..generate_connect_selection()
2616        };
2617
2618        let (connect_txn_proxy, _connect_txn_stream) =
2619            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2620        let options = ConnectedOptions::new(
2621            &mut test_values.common_options,
2622            Box::new(first_ap_state.clone()),
2623            first_connect_selection.target.network_has_multiple_bss,
2624            first_connect_selection.target.network.clone(),
2625            first_connect_selection.target.credential.clone(),
2626            first_connect_selection.reason,
2627            connect_txn_proxy.take_event_stream(),
2628            false,
2629        );
2630        let initial_state = connected_state(test_values.common_options, options);
2631        let fut = run_state_machine(initial_state);
2632        let mut fut = pin!(fut);
2633        let sme_fut = test_values.sme_req_stream.into_future();
2634        let mut sme_fut = pin!(sme_fut);
2635
2636        let disconnect_time =
2637            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2638
2639        // Run the state machine
2640        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2641
2642        // Verify roam monitor request was sent.
2643        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2644            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2645        });
2646
2647        // Run the state machine
2648        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2649
2650        // Run forward to get post connection signals metrics
2651        exec.set_fake_time(fasync::MonotonicInstant::after(
2652            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2653        ));
2654        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2655        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2656            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2657        });
2658
2659        // Run forward to get long duration signals metrics
2660        exec.set_fake_time(fasync::MonotonicInstant::after(
2661            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2662        ));
2663        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2664        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2665            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2666        });
2667
2668        // Run forward to disconnect time
2669        exec.set_fake_time(disconnect_time);
2670        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2671
2672        // Send a different connect request
2673        let mut client = Client::new(test_values.client_req_sender);
2674        client.connect(second_connect_selection.clone()).expect("failed to make request");
2675
2676        // Run the state machine
2677        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2678
2679        // There should be 2 requests to the SME stacked up
2680        // First SME request: disconnect
2681        assert_matches!(
2682            poll_sme_req(&mut exec, &mut sme_fut),
2683            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => {
2684                responder.send().expect("could not send sme response");
2685            }
2686        );
2687        // Progress the state machine
2688        // TODO(https://fxbug.dev/42130926): remove this once the disconnect request is fire-and-forget
2689        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2690        // Second SME request: connect to the second network
2691        let connect_txn_handle = assert_matches!(
2692            poll_sme_req(&mut exec, &mut sme_fut),
2693            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
2694                assert_eq!(req.ssid, second_connect_selection.target.network.ssid.clone().to_vec());
2695                 // Send connection response.
2696                let (_stream, ctrl) = txn.expect("connect txn unused")
2697                    .into_stream_and_control_handle();
2698                ctrl
2699            }
2700        );
2701        connect_txn_handle
2702            .send_on_connect_result(&fake_successful_connect_result())
2703            .expect("failed to send connection completion");
2704        // Progress the state machine
2705        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2706
2707        // Check for a disconnect update
2708        let client_state_update = ClientStateUpdate {
2709            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2710            networks: vec![ClientNetworkState {
2711                id: first_connect_selection.target.network.clone(),
2712                state: fidl_policy::ConnectionState::Disconnected,
2713                status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
2714            }],
2715        };
2716        assert_matches!(
2717            test_values.update_receiver.try_recv(),
2718            Ok(listener::Message::NotifyListeners(updates)) => {
2719            assert_eq!(updates, client_state_update);
2720        });
2721
2722        // Disconnect telemetry event sent
2723        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
2724            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2725                assert!(!track_subsequent_downtime);
2726                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2727                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2728                    assert!(!is_sme_reconnecting);
2729                    assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch));
2730                    assert_eq!(previous_connect_reason, first_connect_selection.reason);
2731                    assert_eq!(ap_state, first_ap_state.clone());
2732                });
2733            });
2734        });
2735
2736        // Check for a connecting update
2737        let client_state_update = ClientStateUpdate {
2738            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2739            networks: vec![ClientNetworkState {
2740                id: types::NetworkIdentifier {
2741                    ssid: second_connect_selection.target.network.ssid.clone(),
2742                    security_type: types::SecurityType::Wpa2,
2743                },
2744                state: fidl_policy::ConnectionState::Connecting,
2745                status: None,
2746            }],
2747        };
2748        assert_matches!(
2749            test_values.update_receiver.try_recv(),
2750            Ok(listener::Message::NotifyListeners(updates)) => {
2751            assert_eq!(updates, client_state_update);
2752        });
2753        // Check for a connected update
2754        let client_state_update = ClientStateUpdate {
2755            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2756            networks: vec![ClientNetworkState {
2757                id: types::NetworkIdentifier {
2758                    ssid: second_connect_selection.target.network.ssid.clone(),
2759                    security_type: types::SecurityType::Wpa2,
2760                },
2761                state: fidl_policy::ConnectionState::Connected,
2762                status: None,
2763            }],
2764        };
2765        assert_matches!(
2766            test_values.update_receiver.try_recv(),
2767            Ok(listener::Message::NotifyListeners(updates)) => {
2768            assert_eq!(updates, client_state_update);
2769        });
2770
2771        // Progress the state machine
2772        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2773
2774        // Ensure no further updates were sent to listeners
2775        assert_matches!(
2776            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
2777            Poll::Pending
2778        );
2779
2780        // Check that the first connection was recorded
2781        let expected_recorded_connection = ConnectionRecord {
2782            id: first_connect_selection.target.network.clone(),
2783            credential: first_connect_selection.target.credential.clone(),
2784            data: PastConnectionData {
2785                bssid: types::Bssid::from(first_bss_desc.bssid),
2786                disconnect_time,
2787                connection_uptime: zx::MonotonicDuration::from_hours(12),
2788                disconnect_reason: types::DisconnectReason::ProactiveNetworkSwitch,
2789                signal_at_disconnect: types::Signal {
2790                    rssi_dbm: first_bss_desc.rssi_dbm,
2791                    snr_db: first_bss_desc.snr_db,
2792                },
2793                // TODO: record average phy rate over connection once available
2794                average_tx_rate: 0,
2795            },
2796        };
2797        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2798            assert_eq!(connection_data, &expected_recorded_connection);
2799        });
2800    }
2801
2802    #[fuchsia::test]
2803    fn connected_state_notified_of_network_disconnect_no_sme_reconnect_short_uptime_no_retry() {
2804        let mut exec = fasync::TestExecutor::new_with_fake_time();
2805        let mut test_values = test_setup();
2806
2807        let connect_selection = generate_connect_selection();
2808        let bss_description =
2809            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2810        let ap_state =
2811            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2812
2813        let (connect_txn_proxy, connect_txn_stream) =
2814            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2815        let connect_txn_handle = connect_txn_stream.control_handle();
2816        let options = ConnectedOptions::new(
2817            &mut test_values.common_options,
2818            Box::new(ap_state.clone()),
2819            connect_selection.target.network_has_multiple_bss,
2820            connect_selection.target.network.clone(),
2821            connect_selection.target.credential.clone(),
2822            connect_selection.reason,
2823            connect_txn_proxy.take_event_stream(),
2824            false,
2825        );
2826        let initial_state = connected_state(test_values.common_options, options);
2827        let fut = run_state_machine(initial_state);
2828        let mut fut = pin!(fut);
2829        let sme_fut = test_values.sme_req_stream.into_future();
2830        let mut sme_fut = pin!(sme_fut);
2831
2832        // Run the state machine
2833        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2834
2835        // Verify roam monitor request was sent.
2836        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2837            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2838        });
2839
2840        // Run the state machine
2841        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2842
2843        // SME notifies Policy of disconnection.
2844        let is_sme_reconnecting = false;
2845        connect_txn_handle
2846            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2847            .expect("failed to send disconnection event");
2848
2849        // Run the state machine
2850        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2851
2852        // Check for a disconnect request to SME
2853        assert_matches!(
2854            poll_sme_req(&mut exec, &mut sme_fut),
2855            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
2856                responder.send().expect("could not send sme response");
2857            }
2858        );
2859
2860        // The state machine should exit since there is no attempt to reconnect.
2861        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2862    }
2863
2864    #[fuchsia::test]
2865    fn connected_state_notified_of_network_disconnect_sme_reconnect_successfully() {
2866        let mut exec = fasync::TestExecutor::new();
2867        let mut test_values = test_setup();
2868
2869        let connect_selection = generate_connect_selection();
2870        let bss_description =
2871            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2872        let ap_state =
2873            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2874
2875        let (connect_txn_proxy, connect_txn_stream) =
2876            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2877        let connect_txn_handle = connect_txn_stream.control_handle();
2878        let options = ConnectedOptions::new(
2879            &mut test_values.common_options,
2880            Box::new(ap_state.clone()),
2881            connect_selection.target.network_has_multiple_bss,
2882            connect_selection.target.network.clone(),
2883            connect_selection.target.credential.clone(),
2884            connect_selection.reason,
2885            connect_txn_proxy.take_event_stream(),
2886            false,
2887        );
2888        let initial_state = connected_state(test_values.common_options, options);
2889        let fut = run_state_machine(initial_state);
2890        let mut fut = pin!(fut);
2891
2892        // Run the state machine
2893        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2894
2895        // SME notifies Policy of disconnection
2896        let is_sme_reconnecting = true;
2897        connect_txn_handle
2898            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2899            .expect("failed to send disconnection event");
2900
2901        // Run the state machine
2902        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2903
2904        // SME notifies Policy that reconnects succeeds
2905        let connect_result =
2906            fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
2907        connect_txn_handle
2908            .send_on_connect_result(&connect_result)
2909            .expect("failed to send reconnection result");
2910
2911        // Run the state machine
2912        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2913
2914        // Check there were no state updates
2915        assert_matches!(test_values.update_receiver.try_recv(), Err(_));
2916    }
2917
2918    #[fuchsia::test]
2919    fn connected_state_notified_of_network_disconnect_sme_reconnect_unsuccessfully() {
2920        let mut exec = fasync::TestExecutor::new_with_fake_time();
2921        let mut test_values = test_setup();
2922        let connect_selection = generate_connect_selection();
2923        let bss_description =
2924            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2925        let ap_state =
2926            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2927
2928        // Set the start time of the connection
2929        let start_time = fasync::MonotonicInstant::now();
2930        exec.set_fake_time(start_time);
2931
2932        let (connect_txn_proxy, connect_txn_stream) =
2933            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2934        let connect_txn_handle = connect_txn_stream.control_handle();
2935        let options = ConnectedOptions::new(
2936            &mut test_values.common_options,
2937            Box::new(ap_state.clone()),
2938            connect_selection.target.network_has_multiple_bss,
2939            connect_selection.target.network.clone(),
2940            connect_selection.target.credential.clone(),
2941            connect_selection.reason,
2942            connect_txn_proxy.take_event_stream(),
2943            false,
2944        );
2945        let initial_state = connected_state(test_values.common_options, options);
2946        let fut = run_state_machine(initial_state);
2947        let mut fut = pin!(fut);
2948        let sme_fut = test_values.sme_req_stream.into_future();
2949        let mut sme_fut = pin!(sme_fut);
2950
2951        // Run the state machine
2952        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2953
2954        // Verify roam monitor request was sent.
2955        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
2956            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2957        });
2958
2959        // Run the state machine
2960        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2961
2962        // Set time to indicate a decent uptime before the disconnect so the AP is retried
2963        exec.set_fake_time(start_time + fasync::MonotonicDuration::from_hours(24));
2964
2965        // SME notifies Policy of disconnection
2966        let is_sme_reconnecting = true;
2967        connect_txn_handle
2968            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2969            .expect("failed to send disconnection event");
2970
2971        // Run the state machine
2972        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2973
2974        // SME notifies Policy that reconnects fails
2975        let connect_result = fidl_sme::ConnectResult {
2976            code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
2977            is_reconnect: true,
2978            ..fake_successful_connect_result()
2979        };
2980        connect_txn_handle
2981            .send_on_connect_result(&connect_result)
2982            .expect("failed to send reconnection result");
2983
2984        // Run the state machine
2985        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2986
2987        // Check for an SME disconnect request
2988        assert_matches!(
2989            poll_sme_req(&mut exec, &mut sme_fut),
2990            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
2991                responder.send().expect("could not send sme response");
2992            }
2993        );
2994
2995        // The state machine should exit since there is no policy attempt to reconnect.
2996        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2997
2998        // Check for a disconnect update
2999        let client_state_update = ClientStateUpdate {
3000            state: fidl_policy::WlanClientState::ConnectionsEnabled,
3001            networks: vec![ClientNetworkState {
3002                id: connect_selection.target.network.clone(),
3003                state: fidl_policy::ConnectionState::Disconnected,
3004                status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
3005            }],
3006        };
3007        assert_matches!(
3008            test_values.update_receiver.try_recv(),
3009            Ok(listener::Message::NotifyListeners(updates)) => {
3010            assert_eq!(updates, client_state_update);
3011        });
3012    }
3013
3014    #[fuchsia::test]
3015    fn connected_state_on_signal_report() {
3016        let mut exec = fasync::TestExecutor::new_with_fake_time();
3017        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3018
3019        let mut test_values = test_setup();
3020
3021        // Verify the status is initialized to default.
3022        let status = test_values.status_reader.read_status().expect("failed to read status");
3023        assert_matches!(status, Status::Disconnected);
3024
3025        // Set initial RSSI and SNR values
3026        let mut connect_selection = generate_connect_selection();
3027        let init_rssi = -40;
3028        let init_snr = 30;
3029        connect_selection.target.bss.signal =
3030            types::Signal { rssi_dbm: init_rssi, snr_db: init_snr };
3031
3032        let mut bss_description =
3033            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3034        bss_description.rssi_dbm = init_rssi;
3035        bss_description.snr_db = init_snr;
3036        connect_selection.target.bss.bss_description = bss_description.clone().into();
3037
3038        let ap_state =
3039            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3040
3041        // Add a PastConnectionData for the connected network to be send in BSS quality data.
3042        let mut past_connections = new_past_connection_list();
3043        let mut past_connection_data = random_connection_data();
3044        past_connection_data.bssid = ieee80211::Bssid::from(bss_description.bssid);
3045        past_connections.add(past_connection_data);
3046        let mut saved_networks_manager = FakeSavedNetworksManager::new();
3047        saved_networks_manager.past_connections_response = past_connections.clone();
3048        test_values.common_options.saved_networks_manager = Arc::new(saved_networks_manager);
3049
3050        // Set up the state machine, starting at the connected state.
3051        let (connect_txn_proxy, connect_txn_stream) =
3052            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3053        let options = ConnectedOptions::new(
3054            &mut test_values.common_options,
3055            Box::new(ap_state.clone()),
3056            connect_selection.target.network_has_multiple_bss,
3057            connect_selection.target.network.clone(),
3058            connect_selection.target.credential.clone(),
3059            connect_selection.reason,
3060            connect_txn_proxy.take_event_stream(),
3061            false,
3062        );
3063        let initial_state = connected_state(test_values.common_options, options);
3064
3065        let connect_txn_handle = connect_txn_stream.control_handle();
3066        let fut = run_state_machine(initial_state);
3067        let mut fut = pin!(fut);
3068        let sme_fut = test_values.sme_req_stream.into_future();
3069        let mut sme_fut = pin!(sme_fut);
3070
3071        // Run the state machine
3072        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3073
3074        let request = test_values
3075            .roam_service_request_receiver
3076            .try_recv()
3077            .expect("error receiving roam service request");
3078        assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor{ mut roam_trigger_data_receiver, .. } => {
3079            // Run the state machine
3080            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3081
3082            // Send the first signal report from SME
3083            let rssi_1 = -50;
3084            let snr_1 = 25;
3085            let fidl_signal_report =
3086                fidl_internal::SignalReportIndication { rssi_dbm: rssi_1, snr_db: snr_1 , tx_rate_500kbps: 0};
3087            connect_txn_handle
3088                .send_on_signal_report(&fidl_signal_report)
3089                .expect("failed to send signal report");
3090            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3091
3092            // Do a quick check that state machine does not exist and there's no disconnect to SME
3093            assert_matches!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending);
3094
3095            // Verify telemetry event
3096            assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
3097                assert_matches!(event, TelemetryEvent::OnSignalReport { .. });
3098            });
3099
3100            // Verify that signal report is sent to the roam monitor
3101            assert_matches!(roam_trigger_data_receiver.try_recv(), Ok(RoamTriggerData::SignalReportInd(_)));
3102
3103            // Verify that the status is updated.
3104            let status = test_values.status_reader.read_status().expect("failed to read status");
3105            assert_eq!(
3106                status,
3107                Status::Connected {
3108                    rssi: rssi_1,
3109                    snr: snr_1,
3110                    channel: ap_state.tracked.channel.primary
3111                }
3112            );
3113
3114            // Send a second signal report with higher RSSI and SNR than the previous reports.
3115            let rssi_2 = -30;
3116            let snr_2 = 35;
3117            let fidl_signal_report =
3118                fidl_internal::SignalReportIndication { rssi_dbm: rssi_2, snr_db: snr_2 , tx_rate_500kbps: 0};
3119            connect_txn_handle
3120                .send_on_signal_report(&fidl_signal_report)
3121                .expect("failed to send signal report");
3122            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3123
3124            // Verify telemetry events;
3125            assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
3126                assert_matches!(event, TelemetryEvent::OnSignalReport { .. });
3127            });
3128
3129            // Verify that signal report is sent to the roam monitor
3130            assert_matches!(roam_trigger_data_receiver.try_recv(), Ok(RoamTriggerData::SignalReportInd(_)));
3131        });
3132    }
3133
3134    #[fuchsia::test]
3135    fn connected_state_on_channel_switched() {
3136        let mut exec = fasync::TestExecutor::new_with_fake_time();
3137        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3138
3139        let mut test_values = test_setup();
3140        let mut telemetry_receiver = test_values.telemetry_receiver;
3141
3142        let connect_selection = generate_connect_selection();
3143        let bss_description =
3144            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3145        let ap_state =
3146            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3147
3148        // Set up the state machine, starting at the connected state.
3149        let (connect_txn_proxy, connect_txn_stream) =
3150            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3151        let options = ConnectedOptions::new(
3152            &mut test_values.common_options,
3153            Box::new(ap_state.clone()),
3154            connect_selection.target.network_has_multiple_bss,
3155            connect_selection.target.network.clone(),
3156            connect_selection.target.credential.clone(),
3157            connect_selection.reason,
3158            connect_txn_proxy.take_event_stream(),
3159            false,
3160        );
3161        let initial_state = connected_state(test_values.common_options, options);
3162
3163        let connect_txn_handle = connect_txn_stream.control_handle();
3164        let fut = run_state_machine(initial_state);
3165        let mut fut = pin!(fut);
3166
3167        // Run the state machine
3168        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3169
3170        // Verify roam monitor request was sent.
3171        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3172            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { ap_state, .. } => {
3173                assert_eq!(ap_state.tracked.channel.primary, bss_description.primary.number)
3174            });
3175        });
3176
3177        // Run the state machine
3178        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3179
3180        let channel_switch_info = fidl_internal::ChannelSwitchInfo {
3181            new_primary_channel: fidl_ieee80211::ChannelNumber {
3182                band: fidl_ieee80211::WlanBand::TwoGhz,
3183                number: 10,
3184            },
3185            bandwidth: fidl_ieee80211::ChannelBandwidth::Cbw20,
3186            vht_secondary_80_channel: fidl_ieee80211::ChannelNumber {
3187                band: fidl_ieee80211::WlanBand::TwoGhz,
3188                number: 0,
3189            },
3190        };
3191        connect_txn_handle
3192            .send_on_channel_switched(&channel_switch_info)
3193            .expect("failed to send signal report");
3194        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3195
3196        // Verify telemetry event
3197        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3198            assert_matches!(event, TelemetryEvent::OnChannelSwitched { info } => {
3199                assert_eq!(info, channel_switch_info);
3200            });
3201        });
3202
3203        // Verify the roam monitor was re-initialized with the new channel
3204        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3205            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { ap_state, .. } => {
3206                assert_eq!(ap_state.tracked.channel.primary, 10)
3207            });
3208        });
3209
3210        // Have SME notify Policy of disconnection so we can see whether the channel in the
3211        // BssDescription has changed.
3212        let is_sme_reconnecting = false;
3213        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
3214        connect_txn_handle
3215            .send_on_disconnect(&fidl_disconnect_info)
3216            .expect("failed to send disconnection event");
3217        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3218
3219        // Verify telemetry event
3220        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3221            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
3222                assert_eq!(info.unwrap().ap_state.tracked.channel.primary, 10);
3223            });
3224        });
3225    }
3226
3227    #[fuchsia::test]
3228    fn connected_state_on_roam_selection() {
3229        let mut exec = fasync::TestExecutor::new_with_fake_time();
3230        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3231
3232        let mut test_values = test_setup();
3233        let sme_fut = test_values.sme_req_stream.into_future();
3234        let mut sme_fut = pin!(sme_fut);
3235
3236        // Set up the state machine, starting at the connected state.
3237        let connect_selection = generate_connect_selection();
3238        let bss_description =
3239            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3240        let ap_state =
3241            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3242        let (connect_txn_proxy, _connect_txn_stream) =
3243            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3244        let options = ConnectedOptions::new(
3245            &mut test_values.common_options,
3246            Box::new(ap_state.clone()),
3247            connect_selection.target.network_has_multiple_bss,
3248            connect_selection.target.network.clone(),
3249            connect_selection.target.credential.clone(),
3250            connect_selection.reason,
3251            connect_txn_proxy.take_event_stream(),
3252            false,
3253        );
3254        let initial_state = connected_state(test_values.common_options, options);
3255        let fut = run_state_machine(initial_state);
3256        let mut fut = pin!(fut);
3257
3258        // Run the state machine.
3259        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3260
3261        // Verify roam monitor selection was sent.
3262        let mut roam_sender;
3263        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3264            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { roam_request_sender, .. } => {
3265                roam_sender = roam_request_sender;
3266            });
3267        });
3268
3269        // Send a roam request to state machine.
3270        let roam_candidate = generate_random_scanned_candidate();
3271        roam_sender
3272            .try_send(PolicyRoamRequest { candidate: roam_candidate.clone(), reasons: vec![] })
3273            .unwrap();
3274
3275        // Run the state machine
3276        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3277
3278        // Verify state machine issues a roam to SME.
3279        assert_matches!(
3280            poll_sme_req(&mut exec, &mut sme_fut),
3281            Poll::Ready(fidl_sme::ClientSmeRequest::Roam{ req, ..}) => {
3282                assert_eq!(req.bss_description, Sequestered::release(roam_candidate.clone().bss.bss_description));
3283            }
3284        );
3285
3286        // Verify roam attempt telemetry event.
3287        assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
3288            assert_matches!(event, TelemetryEvent::PolicyRoamAttempt { request, connected_duration } => {
3289                assert_eq!(request.candidate, roam_candidate);
3290                assert_eq!(request.reasons, vec![]);
3291                assert_eq!(connected_duration, zx::Duration::from_minutes(0));
3292
3293            });
3294        });
3295    }
3296
3297    #[fuchsia::test]
3298    fn connected_state_on_roam_result_success() {
3299        let mut exec = fasync::TestExecutor::new_with_fake_time();
3300        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3301
3302        let mut test_values = test_setup();
3303        let mut telemetry_receiver = test_values.telemetry_receiver;
3304
3305        let connect_selection = generate_connect_selection();
3306        let bss_description =
3307            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3308        let ap_state =
3309            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3310
3311        // Set up the state machine, starting at the connected state.
3312        let (connect_txn_proxy, connect_txn_stream) =
3313            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3314        let mut options = ConnectedOptions::new(
3315            &mut test_values.common_options,
3316            Box::new(ap_state.clone()),
3317            connect_selection.target.network_has_multiple_bss,
3318            connect_selection.target.network.clone(),
3319            connect_selection.target.credential.clone(),
3320            connect_selection.reason,
3321            connect_txn_proxy.take_event_stream(),
3322            false,
3323        );
3324        // Set the pending roam request, so we don't immediately exit when a result comes in.
3325        let policy_request = generate_policy_roam_request([1, 1, 1, 1, 1, 1].into());
3326        options.pending_roam = Some(policy_request.clone().into());
3327        let initial_state = connected_state(test_values.common_options, options);
3328
3329        let connect_txn_handle = connect_txn_stream.control_handle();
3330        let fut = run_state_machine(initial_state);
3331        let mut fut = pin!(fut);
3332
3333        // Run the state machine
3334        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3335
3336        // Verify roam monitor request was sent.
3337        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3338            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
3339        });
3340
3341        // Send a successful roam result
3342        let bss_desc = random_fidl_bss_description!();
3343        let roam_result = fidl_sme::RoamResult {
3344            bssid: [1, 1, 1, 1, 1, 1],
3345            status_code: fidl_ieee80211::StatusCode::Success,
3346            original_association_maintained: false,
3347            bss_description: Some(Box::new(bss_desc.clone())),
3348            disconnect_info: None,
3349            is_credential_rejected: false,
3350        };
3351        connect_txn_handle.send_on_roam_result(&roam_result).expect("failed to send roam result");
3352        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3353
3354        // Verify the roam monitor was re-initialized with the new BSS
3355        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3356            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { ap_state, .. } => {
3357                assert_eq!(ap_state.original().bssid.to_array(), bss_desc.bssid);
3358            });
3359        });
3360
3361        // Verify a disconnect was logged to saved networks manager
3362        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [ConnectionRecord {id, credential, data}] => {
3363            assert_eq!(id, &connect_selection.target.network.clone());
3364            assert_eq!(credential, &connect_selection.target.credential.clone());
3365            assert_matches!(data, PastConnectionData {bssid, disconnect_reason, ..} => {
3366                assert_eq!(bssid, &connect_selection.target.bss.bssid);
3367                assert_eq!(disconnect_reason, &types::DisconnectReason::Unknown);
3368            })
3369        });
3370
3371        // Verify the successful connect result was logged to saved networks manager
3372        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
3373            let expected_connect_result = ConnectResultRecord {
3374                 id: connect_selection.target.network.clone(),
3375                 credential: connect_selection.target.credential.clone(),
3376                 bssid: types::Bssid::from(roam_result.bssid),
3377                 connect_result: fidl_sme::ConnectResult {
3378                    code: roam_result.status_code,
3379                    is_credential_rejected: roam_result.is_credential_rejected,
3380                    is_reconnect: false,
3381                 },
3382                 scan_type: types::ScanObservation::Unknown,
3383            };
3384            assert_eq!(data, &expected_connect_result);
3385        });
3386
3387        // Verify telemetry event for roam result
3388        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3389            assert_matches!(event, TelemetryEvent::PolicyInitiatedRoamResult { result, .. } => {
3390                assert_eq!(result, roam_result);
3391            });
3392        });
3393
3394        // Explicitly verify there is _not_ a disconnect metric logged, since we have not exited the
3395        // ESS.
3396        assert_matches!(telemetry_receiver.try_recv(), Err(_));
3397
3398        // Run time forward past the timeout for the pending roam request.
3399        exec.set_fake_time(fasync::MonotonicInstant::after(
3400            PENDING_ROAM_TIMEOUT + zx::MonotonicDuration::from_millis(1),
3401        ));
3402
3403        // Run the state machine and verify that it does NOT fire the pending timer and exit.
3404        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3405    }
3406
3407    #[fuchsia::test]
3408    fn connected_state_on_roam_result_failed_original_association_maintained() {
3409        let mut exec = fasync::TestExecutor::new_with_fake_time();
3410        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3411
3412        let mut test_values = test_setup();
3413        let mut telemetry_receiver = test_values.telemetry_receiver;
3414
3415        let connect_selection = generate_connect_selection();
3416        let bss_description =
3417            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3418        let ap_state =
3419            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3420
3421        // Set up the state machine, starting at the connected state.
3422        let (connect_txn_proxy, connect_txn_stream) =
3423            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3424        let mut options = ConnectedOptions::new(
3425            &mut test_values.common_options,
3426            Box::new(ap_state.clone()),
3427            connect_selection.target.network_has_multiple_bss,
3428            connect_selection.target.network.clone(),
3429            connect_selection.target.credential.clone(),
3430            connect_selection.reason,
3431            connect_txn_proxy.take_event_stream(),
3432            false,
3433        );
3434        // Set the pending roam request, so we don't immediately exit when a result comes in.
3435        let policy_request = generate_policy_roam_request([1, 1, 1, 1, 1, 1].into());
3436        options.pending_roam = Some(policy_request.clone().into());
3437        let initial_state = connected_state(test_values.common_options, options);
3438
3439        let connect_txn_handle = connect_txn_stream.control_handle();
3440        let fut = run_state_machine(initial_state);
3441        let mut fut = pin!(fut);
3442
3443        // Run the state machine
3444        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3445
3446        // Verify roam monitor request was sent.
3447        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3448            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
3449        });
3450
3451        // Send a failed roam result, where the original association was maintained.
3452        let roam_result = fidl_sme::RoamResult {
3453            bssid: [1, 1, 1, 1, 1, 1],
3454            status_code: fidl_ieee80211::StatusCode::JoinFailure,
3455            original_association_maintained: true,
3456            bss_description: Some(Box::new(bss_description.clone())),
3457            disconnect_info: None,
3458            is_credential_rejected: false,
3459        };
3460        connect_txn_handle.send_on_roam_result(&roam_result).expect("failed to send roam result");
3461        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3462
3463        // Verify the failed connect result was logged to saved networks manager
3464        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
3465            let expected_connect_result = ConnectResultRecord {
3466                 id: connect_selection.target.network.clone(),
3467                 credential: connect_selection.target.credential.clone(),
3468                 bssid: types::Bssid::from(roam_result.bssid),
3469                 connect_result: fidl_sme::ConnectResult {
3470                    code: roam_result.status_code,
3471                    is_credential_rejected: roam_result.is_credential_rejected,
3472                    is_reconnect: false,
3473                 },
3474                 scan_type: types::ScanObservation::Unknown,
3475            };
3476            assert_eq!(data, &expected_connect_result);
3477        });
3478
3479        // A defect should be logged.
3480        assert_matches!(
3481            test_values.defect_receiver.try_recv(),
3482            Ok(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))
3483        );
3484
3485        // Verify telemetry event for roam result
3486        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3487            assert_matches!(event, TelemetryEvent::PolicyInitiatedRoamResult { result, .. } => {
3488                assert_eq!(result, roam_result);
3489            });
3490        });
3491
3492        // Explicitly verify there is _not_ a disconnect metric logged, since we have not exited the
3493        // ESS.
3494        assert_matches!(telemetry_receiver.try_recv(), Err(_));
3495
3496        // Verify the roam monitor was _not_ re-initialized.
3497        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Err(_));
3498
3499        // Run time forward past the timeout for the pending roam request.
3500        exec.set_fake_time(fasync::MonotonicInstant::after(
3501            PENDING_ROAM_TIMEOUT + zx::MonotonicDuration::from_millis(1),
3502        ));
3503
3504        // Run the state machine and verify that it does NOT fire the pending timer and exit.
3505        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3506    }
3507
3508    #[fuchsia::test]
3509    fn connected_state_on_roam_result_failed_and_disconnected() {
3510        let mut exec = fasync::TestExecutor::new_with_fake_time();
3511        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3512
3513        let mut test_values = test_setup();
3514        let mut telemetry_receiver = test_values.telemetry_receiver;
3515        let sme_fut = test_values.sme_req_stream.into_future();
3516        let mut sme_fut = pin!(sme_fut);
3517
3518        let connect_selection = generate_connect_selection();
3519        let bss_description =
3520            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3521        let ap_state =
3522            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3523
3524        // Set up the state machine, starting at the connected state.
3525        let (connect_txn_proxy, connect_txn_stream) =
3526            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3527        let mut options = ConnectedOptions::new(
3528            &mut test_values.common_options,
3529            Box::new(ap_state.clone()),
3530            connect_selection.target.network_has_multiple_bss,
3531            connect_selection.target.network.clone(),
3532            connect_selection.target.credential.clone(),
3533            connect_selection.reason,
3534            connect_txn_proxy.take_event_stream(),
3535            false,
3536        );
3537        // Set the pending roam request, so we don't immediately exit when a result comes in.
3538        let policy_request = generate_policy_roam_request([1, 1, 1, 1, 1, 1].into());
3539        options.pending_roam = Some(policy_request.clone().into());
3540        let initial_state = connected_state(test_values.common_options, options);
3541
3542        let connect_txn_handle = connect_txn_stream.control_handle();
3543        let fut = run_state_machine(initial_state);
3544        let mut fut = pin!(fut);
3545
3546        // Run the state machine
3547        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3548
3549        // Verify roam monitor request was sent.
3550        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3551            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
3552        });
3553
3554        // Send a failed roam result, where the original association was *NOT* maintained.
3555        let disconnect_info = fidl_sme::DisconnectInfo {
3556            is_sme_reconnecting: false,
3557            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
3558                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
3559                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
3560            }),
3561        };
3562        let roam_result = fidl_sme::RoamResult {
3563            bssid: [1, 1, 1, 1, 1, 1],
3564            status_code: fidl_ieee80211::StatusCode::JoinFailure,
3565            original_association_maintained: false,
3566            bss_description: None,
3567            disconnect_info: Some(Box::new(disconnect_info)),
3568            is_credential_rejected: false,
3569        };
3570        connect_txn_handle.send_on_roam_result(&roam_result).expect("failed to send roam result");
3571        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3572
3573        // Verify the failed connect result was logged to saved networks manager
3574        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
3575            let expected_connect_result = ConnectResultRecord {
3576                 id: connect_selection.target.network.clone(),
3577                 credential: connect_selection.target.credential.clone(),
3578                 bssid: types::Bssid::from(roam_result.bssid),
3579                 connect_result: fidl_sme::ConnectResult {
3580                    code: roam_result.status_code,
3581                    is_credential_rejected: roam_result.is_credential_rejected,
3582                    is_reconnect: false,
3583                 },
3584                 scan_type: types::ScanObservation::Unknown,
3585            };
3586            assert_eq!(data, &expected_connect_result);
3587        });
3588
3589        // Verify a disconnect was logged to saved networks manager
3590        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [ConnectionRecord {id, credential, data}] => {
3591            assert_eq!(id, &connect_selection.target.network.clone());
3592            assert_eq!(credential, &connect_selection.target.credential.clone());
3593            assert_matches!(data, PastConnectionData {bssid, disconnect_reason, ..} => {
3594                assert_eq!(bssid, &connect_selection.target.bss.bssid);
3595                assert_eq!(disconnect_reason, &types::DisconnectReason::Unknown);
3596            })
3597        });
3598
3599        // Verify telemetry event for disconnect
3600        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3601            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
3602                assert_eq!(info.unwrap().disconnect_source, disconnect_info.disconnect_source);
3603            });
3604        });
3605
3606        // Verify telemetry event for roam result
3607        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3608            assert_matches!(event, TelemetryEvent::PolicyInitiatedRoamResult { result, .. } => {
3609                assert_eq!(result, roam_result);
3610            });
3611        });
3612
3613        // A defect should be logged.
3614        assert_matches!(
3615            test_values.defect_receiver.try_recv(),
3616            Ok(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))
3617        );
3618
3619        // Check for an SME disconnect request
3620        assert_matches!(
3621            poll_sme_req(&mut exec, &mut sme_fut),
3622            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect { .. })
3623        );
3624
3625        // Run time forward past the timeout for the pending roam request.
3626        exec.set_fake_time(fasync::MonotonicInstant::after(
3627            PENDING_ROAM_TIMEOUT + zx::MonotonicDuration::from_millis(1),
3628        ));
3629
3630        // Run the state machine and verify that it does NOT fire the pending timer and exit.
3631        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3632    }
3633    #[fuchsia::test]
3634    fn connected_state_on_unexpected_policy_roam_result_disconnects() {
3635        let mut exec = fasync::TestExecutor::new_with_fake_time();
3636        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3637
3638        let mut test_values = test_setup();
3639        let mut telemetry_receiver = test_values.telemetry_receiver;
3640        let sme_fut = test_values.sme_req_stream.into_future();
3641        let mut sme_fut = pin!(sme_fut);
3642
3643        let connect_selection = generate_connect_selection();
3644        let bss_description =
3645            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3646        let ap_state =
3647            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3648
3649        // Set up the state machine, starting at the connected state.
3650        let (connect_txn_proxy, connect_txn_stream) =
3651            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3652        let mut options = ConnectedOptions::new(
3653            &mut test_values.common_options,
3654            Box::new(ap_state.clone()),
3655            connect_selection.target.network_has_multiple_bss,
3656            connect_selection.target.network.clone(),
3657            connect_selection.target.credential.clone(),
3658            connect_selection.reason,
3659            connect_txn_proxy.take_event_stream(),
3660            false,
3661        );
3662        // Set the pending roam request for some BSSID.
3663        let policy_request = generate_policy_roam_request([1, 1, 1, 1, 1, 1].into());
3664        options.pending_roam = Some(policy_request.into());
3665        let initial_state = connected_state(test_values.common_options, options);
3666
3667        let connect_txn_handle = connect_txn_stream.control_handle();
3668        let fut = run_state_machine(initial_state);
3669        let mut fut = pin!(fut);
3670
3671        // Run the state machine
3672        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3673
3674        // Verify roam monitor request was sent.
3675        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3676            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
3677        });
3678
3679        // Send a roam result with a BSSID that does NOT match the roam request.
3680        let bss_desc = random_fidl_bss_description!();
3681        let roam_result = fidl_sme::RoamResult {
3682            bssid: [2, 2, 2, 2, 2, 2],
3683            status_code: fidl_ieee80211::StatusCode::Success,
3684            original_association_maintained: false,
3685            bss_description: Some(Box::new(bss_desc.clone())),
3686            disconnect_info: None,
3687            is_credential_rejected: false,
3688        };
3689        connect_txn_handle.send_on_roam_result(&roam_result).expect("failed to send roam result");
3690
3691        // Run forward the state machine.
3692        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3693
3694        // Verify a disconnect request is sent to SME.
3695        assert_matches!(
3696            poll_sme_req(&mut exec, &mut sme_fut),
3697            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
3698                responder.send().expect("could not send sme response");
3699            }
3700        );
3701
3702        // Ensure the state machine exits once the disconnect is processed.
3703        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
3704
3705        // Verify a disconnect was logged to saved networks manager
3706        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [ConnectionRecord {id, credential, data}] => {
3707            assert_eq!(id, &connect_selection.target.network.clone());
3708            assert_eq!(credential, &connect_selection.target.credential.clone());
3709            assert_matches!(data, PastConnectionData {bssid, disconnect_reason, ..} => {
3710                assert_eq!(bssid, &connect_selection.target.bss.bssid);
3711                assert_eq!(disconnect_reason, &types::DisconnectReason::Unknown);
3712            })
3713        });
3714
3715        // Verify a disconnect event was logged to telemetry
3716        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3717            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
3718                assert_eq!(info.unwrap().disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown));
3719            });
3720        });
3721
3722        // Verify a disconnected listener update is sent.
3723        assert_matches!(
3724            test_values.update_receiver.try_recv(),
3725            Ok(listener::Message::NotifyListeners(ClientStateUpdate {
3726                state: fidl_policy::WlanClientState::ConnectionsEnabled,
3727                networks
3728            })) => {
3729                assert_eq!(networks.len(), 1);
3730                assert_eq!(networks[0].id, connect_selection.target.network);
3731                assert_eq!(networks[0].state, fidl_policy::ConnectionState::Disconnected);
3732                assert_eq!(networks[0].status, Some(fidl_policy::DisconnectStatus::ConnectionFailed));
3733            }
3734        );
3735    }
3736
3737    #[fuchsia::test]
3738    fn connected_state_on_unexpected_policy_roam_result_exits_on_broken_sme() {
3739        let mut exec = fasync::TestExecutor::new_with_fake_time();
3740        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3741
3742        let mut test_values = test_setup();
3743        let mut telemetry_receiver = test_values.telemetry_receiver;
3744
3745        let connect_selection = generate_connect_selection();
3746        let bss_description =
3747            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3748        let ap_state =
3749            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3750
3751        // Set up the state machine, starting at the connected state.
3752        let (connect_txn_proxy, connect_txn_stream) =
3753            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3754        let mut options = ConnectedOptions::new(
3755            &mut test_values.common_options,
3756            Box::new(ap_state.clone()),
3757            connect_selection.target.network_has_multiple_bss,
3758            connect_selection.target.network.clone(),
3759            connect_selection.target.credential.clone(),
3760            connect_selection.reason,
3761            connect_txn_proxy.take_event_stream(),
3762            false,
3763        );
3764        // Set the pending roam request for some BSSID.
3765        let policy_request = generate_policy_roam_request([1, 1, 1, 1, 1, 1].into());
3766        options.pending_roam = Some(policy_request.into());
3767        let initial_state = connected_state(test_values.common_options, options);
3768
3769        let connect_txn_handle = connect_txn_stream.control_handle();
3770        let fut = run_state_machine(initial_state);
3771        let mut fut = pin!(fut);
3772
3773        // Run the state machine
3774        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3775
3776        // Verify roam monitor request was sent.
3777        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3778            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
3779        });
3780
3781        // Break the SME by dropping the server end of the SME stream, so it causes an error
3782        drop(test_values.sme_req_stream);
3783
3784        // Send a roam result with a BSSID that does NOT match the roam request.
3785        let bss_desc = random_fidl_bss_description!();
3786        let roam_result = fidl_sme::RoamResult {
3787            bssid: [2, 2, 2, 2, 2, 2],
3788            status_code: fidl_ieee80211::StatusCode::Success,
3789            original_association_maintained: false,
3790            bss_description: Some(Box::new(bss_desc.clone())),
3791            disconnect_info: None,
3792            is_credential_rejected: false,
3793        };
3794        connect_txn_handle.send_on_roam_result(&roam_result).expect("failed to send roam result");
3795
3796        // Ensure the state machine exits.
3797        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
3798
3799        // Verify a disconnect was logged to saved networks manager
3800        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [ConnectionRecord {id, credential, data}] => {
3801            assert_eq!(id, &connect_selection.target.network.clone());
3802            assert_eq!(credential, &connect_selection.target.credential.clone());
3803            assert_matches!(data, PastConnectionData {bssid, disconnect_reason, ..} => {
3804                assert_eq!(bssid, &connect_selection.target.bss.bssid);
3805                assert_eq!(disconnect_reason, &types::DisconnectReason::Unknown);
3806            })
3807        });
3808
3809        // Verify a disconnect event was logged to telemetry
3810        assert_matches!(telemetry_receiver.try_recv(), Ok(event) => {
3811            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
3812                assert_eq!(info.unwrap().disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown));
3813            });
3814        });
3815
3816        // Verify a disconnected listener update is sent.
3817        assert_matches!(
3818            test_values.update_receiver.try_recv(),
3819            Ok(listener::Message::NotifyListeners(ClientStateUpdate {
3820                state: fidl_policy::WlanClientState::ConnectionsEnabled,
3821                networks
3822            })) => {
3823                assert_eq!(networks.len(), 1);
3824                assert_eq!(networks[0].id, connect_selection.target.network);
3825                assert_eq!(networks[0].state, fidl_policy::ConnectionState::Disconnected);
3826                assert_eq!(networks[0].status, Some(fidl_policy::DisconnectStatus::ConnectionFailed));
3827            }
3828        );
3829    }
3830
3831    #[fuchsia::test]
3832    fn connected_state_pending_roam_expires() {
3833        let mut exec = fasync::TestExecutor::new_with_fake_time();
3834
3835        let mut test_values = test_setup();
3836        let sme_fut = test_values.sme_req_stream.into_future();
3837        let mut sme_fut = pin!(sme_fut);
3838
3839        let connect_selection = generate_connect_selection();
3840        let bss_description =
3841            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3842        let ap_state =
3843            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3844
3845        let (connect_txn_proxy, _connect_txn_stream) =
3846            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3847        let options = ConnectedOptions::new(
3848            &mut test_values.common_options,
3849            Box::new(ap_state.clone()),
3850            connect_selection.target.network_has_multiple_bss,
3851            connect_selection.target.network.clone(),
3852            connect_selection.target.credential.clone(),
3853            connect_selection.reason,
3854            connect_txn_proxy.take_event_stream(),
3855            false,
3856        );
3857        // Set the pending roam request for some BSSID.
3858        let initial_state = connected_state(test_values.common_options, options);
3859        let fut = run_state_machine(initial_state);
3860        let mut fut = pin!(fut);
3861
3862        // Verify roam monitor init was sent.
3863        let mut roam_sender;
3864        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
3865            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { roam_request_sender, .. } => {
3866                roam_sender = roam_request_sender;
3867            });
3868        });
3869
3870        // Run the state machine
3871        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3872
3873        // Send a roam request to state machine.
3874        let roam_candidate = generate_random_scanned_candidate();
3875        roam_sender
3876            .try_send(PolicyRoamRequest { candidate: roam_candidate.clone(), reasons: vec![] })
3877            .unwrap();
3878
3879        // Run the state machine
3880        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3881
3882        // Verify state machine issues a roam to SME.
3883        assert_matches!(
3884            poll_sme_req(&mut exec, &mut sme_fut),
3885            Poll::Ready(fidl_sme::ClientSmeRequest::Roam{ req, ..}) => {
3886                assert_eq!(req.bss_description, Sequestered::release(roam_candidate.clone().bss.bss_description));
3887            }
3888        );
3889
3890        // Verify roam attempt telemetry event.
3891        assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
3892            assert_matches!(event, TelemetryEvent::PolicyRoamAttempt { request, .. } => {
3893                assert_eq!(request.candidate,
3894                    roam_candidate);
3895                assert_eq!(request.reasons, vec![]);
3896            });
3897        });
3898
3899        // Run time forward past the timeout for the pending roam request.
3900        exec.set_fake_time(fasync::MonotonicInstant::after(
3901            PENDING_ROAM_TIMEOUT + zx::MonotonicDuration::from_millis(1),
3902        ));
3903
3904        // Run forward the state machine.
3905        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3906
3907        // Verify a disconnect request is sent to SME.
3908        assert_matches!(
3909            poll_sme_req(&mut exec, &mut sme_fut),
3910            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
3911                responder.send().expect("could not send sme response");
3912            }
3913        );
3914
3915        // Ensure the state machine exits once the disconnect is processed.
3916        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
3917
3918        // Verify a disconnect was logged to saved networks manager
3919        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [ConnectionRecord {id, credential, data}] => {
3920            assert_eq!(id, &connect_selection.target.network.clone());
3921            assert_eq!(credential, &connect_selection.target.credential.clone());
3922            assert_matches!(data, PastConnectionData {bssid, disconnect_reason, ..} => {
3923                assert_eq!(bssid, &connect_selection.target.bss.bssid);
3924                assert_eq!(disconnect_reason, &types::DisconnectReason::Unknown);
3925            })
3926        });
3927
3928        // Verify a disconnect event was logged to telemetry
3929        assert_matches!(test_values.telemetry_receiver.try_recv(), Ok(event) => {
3930            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
3931                assert_eq!(info.unwrap().disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown));
3932            });
3933        });
3934
3935        // Verify a disconnected listener update is sent.
3936        assert_matches!(
3937            test_values.update_receiver.try_recv(),
3938            Ok(listener::Message::NotifyListeners(ClientStateUpdate {
3939                state: fidl_policy::WlanClientState::ConnectionsEnabled,
3940                networks
3941            })) => {
3942                assert_eq!(networks.len(), 1);
3943                assert_eq!(networks[0].id, connect_selection.target.network);
3944                assert_eq!(networks[0].state, fidl_policy::ConnectionState::Disconnected);
3945                assert_eq!(networks[0].status, Some(fidl_policy::DisconnectStatus::ConnectionFailed));
3946            }
3947        );
3948    }
3949
3950    #[fuchsia::test]
3951    fn disconnecting_state_completes_and_exits() {
3952        let mut exec = fasync::TestExecutor::new();
3953        let mut test_values = test_setup();
3954
3955        let (sender, _) = oneshot::channel();
3956        let disconnecting_options = DisconnectingOptions {
3957            disconnect_responder: Some(sender),
3958            previous_network: None,
3959            next_network: None,
3960            reason: types::DisconnectReason::RegulatoryRegionChange,
3961        };
3962        let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
3963        let fut = run_state_machine(initial_state);
3964        let mut fut = pin!(fut);
3965        let sme_fut = test_values.sme_req_stream.into_future();
3966        let mut sme_fut = pin!(sme_fut);
3967
3968        // Run the state machine
3969        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3970
3971        // Ensure a disconnect request is sent to the SME
3972        assert_matches!(
3973            poll_sme_req(&mut exec, &mut sme_fut),
3974            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::RegulatoryRegionChange }) => {
3975                responder.send().expect("could not send sme response");
3976            }
3977        );
3978
3979        // Ensure the state machine exits once the disconnect is processed.
3980        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
3981
3982        // The state machine should have sent a listener update
3983        assert_matches!(
3984            test_values.update_receiver.try_recv(),
3985            Ok(listener::Message::NotifyListeners(ClientStateUpdate {
3986                state: fidl_policy::WlanClientState::ConnectionsEnabled,
3987                networks
3988            })) => {
3989                assert!(networks.is_empty());
3990            }
3991        );
3992    }
3993
3994    #[fuchsia::test]
3995    fn disconnecting_state_completes_disconnect_to_connecting() {
3996        let mut exec = fasync::TestExecutor::new();
3997        let mut test_values = test_setup();
3998
3999        let previous_connect_selection = generate_connect_selection();
4000        let next_connect_selection = generate_connect_selection();
4001
4002        let bss_description =
4003            Sequestered::release(next_connect_selection.target.bss.bss_description.clone());
4004
4005        let (disconnect_sender, mut disconnect_receiver) = oneshot::channel();
4006        let connecting_options = ConnectingOptions {
4007            connect_selection: next_connect_selection.clone(),
4008            attempt_counter: 0,
4009        };
4010        // Include both a "previous" and "next" network
4011        let disconnecting_options = DisconnectingOptions {
4012            disconnect_responder: Some(disconnect_sender),
4013            previous_network: Some((
4014                previous_connect_selection.target.network.clone(),
4015                fidl_policy::DisconnectStatus::ConnectionStopped,
4016            )),
4017            next_network: Some(connecting_options),
4018            reason: types::DisconnectReason::ProactiveNetworkSwitch,
4019        };
4020        let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
4021        let fut = run_state_machine(initial_state);
4022        let mut fut = pin!(fut);
4023        let sme_fut = test_values.sme_req_stream.into_future();
4024        let mut sme_fut = pin!(sme_fut);
4025
4026        // Run the state machine
4027        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4028
4029        // Ensure a disconnect request is sent to the SME
4030        assert_matches!(
4031            poll_sme_req(&mut exec, &mut sme_fut),
4032            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => {
4033                responder.send().expect("could not send sme response");
4034            }
4035        );
4036
4037        // Progress the state machine
4038        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4039
4040        // Check for a disconnect update and the disconnect responder
4041        let client_state_update = ClientStateUpdate {
4042            state: fidl_policy::WlanClientState::ConnectionsEnabled,
4043            networks: vec![ClientNetworkState {
4044                id: previous_connect_selection.target.network.clone(),
4045                state: fidl_policy::ConnectionState::Disconnected,
4046                status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
4047            }],
4048        };
4049        assert_matches!(
4050            test_values.update_receiver.try_recv(),
4051            Ok(listener::Message::NotifyListeners(updates)) => {
4052            assert_eq!(updates, client_state_update);
4053        });
4054
4055        assert_matches!(exec.run_until_stalled(&mut disconnect_receiver), Poll::Ready(Ok(())));
4056
4057        // Ensure a connect request is sent to the SME
4058        assert_matches!(
4059            poll_sme_req(&mut exec, &mut sme_fut),
4060            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
4061                assert_eq!(req.ssid, next_connect_selection.target.network.ssid.clone().to_vec());
4062                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
4063                assert_eq!(req.bss_description, bss_description.clone());
4064                assert_eq!(req.multiple_bss_candidates, next_connect_selection.target.network_has_multiple_bss);
4065                 // Send connection response.
4066                let (_stream, ctrl) = txn.expect("connect txn unused")
4067                    .into_stream_and_control_handle();
4068                ctrl
4069                    .send_on_connect_result(&fake_successful_connect_result())
4070                    .expect("failed to send connection completion");
4071            }
4072        );
4073    }
4074
4075    #[fuchsia::test]
4076    fn disconnecting_state_has_broken_sme() {
4077        let mut exec = fasync::TestExecutor::new();
4078        let test_values = test_setup();
4079
4080        let (sender, mut receiver) = oneshot::channel();
4081        let disconnecting_options = DisconnectingOptions {
4082            disconnect_responder: Some(sender),
4083            previous_network: None,
4084            next_network: None,
4085            reason: types::DisconnectReason::NetworkConfigUpdated,
4086        };
4087        let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
4088        let fut = run_state_machine(initial_state);
4089        let mut fut = pin!(fut);
4090
4091        // Break the SME by dropping the server end of the SME stream, so it causes an error
4092        drop(test_values.sme_req_stream);
4093
4094        // Ensure the state machine exits
4095        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4096
4097        // Expect the responder to have an error
4098        assert_matches!(exec.run_until_stalled(&mut receiver), Poll::Ready(Err(_)));
4099    }
4100
4101    #[fuchsia::test]
4102    fn serve_loop_handles_startup() {
4103        let mut exec = fasync::TestExecutor::new();
4104        let test_values = test_setup();
4105        let sme_proxy = test_values.common_options.proxy;
4106        let sme_event_stream = sme_proxy.take_event_stream();
4107        let (_client_req_sender, client_req_stream) = mpsc::channel(1);
4108        let sme_fut = test_values.sme_req_stream.into_future();
4109        let mut sme_fut = pin!(sme_fut);
4110
4111        // Create a connect request so that the state machine does not immediately exit.
4112        let connect_selection = generate_connect_selection();
4113
4114        let fut = serve(
4115            0,
4116            sme_proxy,
4117            sme_event_stream,
4118            client_req_stream,
4119            test_values.common_options.update_sender,
4120            test_values.common_options.saved_networks_manager,
4121            Some(connect_selection),
4122            test_values.common_options.telemetry_sender,
4123            test_values.common_options.defect_sender,
4124            test_values.common_options.roam_manager,
4125            test_values.common_options.status_publisher,
4126        );
4127        let mut fut = pin!(fut);
4128
4129        // Run the state machine so it sends the initial SME disconnect request.
4130        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4131        assert_matches!(
4132            poll_sme_req(&mut exec, &mut sme_fut),
4133            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
4134                responder.send().expect("could not send sme response");
4135            }
4136        );
4137
4138        // Run the future again and ensure that it has not exited after receiving the response.
4139        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4140    }
4141
4142    #[fuchsia::test]
4143    fn serve_loop_handles_sme_disappearance() {
4144        let mut exec = fasync::TestExecutor::new();
4145        let mut test_values = test_setup();
4146        let (_client_req_sender, client_req_stream) = mpsc::channel(1);
4147
4148        // Make our own SME proxy for this test
4149        let (sme_proxy, sme_server) = create_proxy::<fidl_sme::ClientSmeMarker>();
4150        let (sme_req_stream, sme_control_handle) = sme_server.into_stream_and_control_handle();
4151
4152        let sme_fut = sme_req_stream.into_future();
4153        let mut sme_fut = pin!(sme_fut);
4154
4155        let sme_event_stream = sme_proxy.take_event_stream();
4156
4157        // Create a connect request so that the state machine does not immediately exit.
4158        let connect_selection = generate_connect_selection();
4159
4160        let fut = serve(
4161            0,
4162            SmeForClientStateMachine::new(
4163                sme_proxy,
4164                0,
4165                test_values.common_options.defect_sender.clone(),
4166            ),
4167            sme_event_stream,
4168            client_req_stream,
4169            test_values.common_options.update_sender,
4170            test_values.common_options.saved_networks_manager,
4171            Some(connect_selection),
4172            test_values.common_options.telemetry_sender,
4173            test_values.common_options.defect_sender,
4174            test_values.common_options.roam_manager,
4175            test_values.common_options.status_publisher,
4176        );
4177        let mut fut = pin!(fut);
4178
4179        // Run the state machine so it sends the initial SME disconnect request.
4180        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4181        assert_matches!(
4182            poll_sme_req(&mut exec, &mut sme_fut),
4183            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
4184                responder.send().expect("could not send sme response");
4185            }
4186        );
4187
4188        // Run the future again and ensure that it has not exited after receiving the response.
4189        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4190
4191        sme_control_handle.shutdown_with_epitaph(zx::Status::UNAVAILABLE);
4192
4193        // Ensure the state machine has no further actions and is exited
4194        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4195
4196        // Verify that a disconnect event was logged on exit.
4197        let mut telemetry_events = Vec::new();
4198        while let Ok(event) = test_values.telemetry_receiver.try_recv() {
4199            telemetry_events.push(event)
4200        }
4201
4202        assert_matches!(
4203            telemetry_events.last(),
4204            Some(&TelemetryEvent::Disconnected { track_subsequent_downtime: false, info: None })
4205        );
4206    }
4207
4208    #[fuchsia::test]
4209    fn serve_loop_handles_disconnect() {
4210        let mut exec = fasync::TestExecutor::new();
4211        let mut test_values = test_setup();
4212        let sme_proxy = test_values.common_options.proxy;
4213        let sme_event_stream = sme_proxy.take_event_stream();
4214        let (client_req_sender, client_req_stream) = mpsc::channel(1);
4215        let sme_fut = test_values.sme_req_stream.into_future();
4216        let mut sme_fut = pin!(sme_fut);
4217
4218        // Create a connect request so that the state machine does not immediately exit.
4219        let connect_selection = generate_connect_selection();
4220        let fut = serve(
4221            0,
4222            sme_proxy,
4223            sme_event_stream,
4224            client_req_stream,
4225            test_values.common_options.update_sender,
4226            test_values.common_options.saved_networks_manager,
4227            Some(connect_selection),
4228            test_values.common_options.telemetry_sender,
4229            test_values.common_options.defect_sender,
4230            test_values.common_options.roam_manager,
4231            test_values.common_options.status_publisher,
4232        );
4233        let mut fut = pin!(fut);
4234
4235        // Run the state machine so it sends the initial SME disconnect request.
4236        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4237        assert_matches!(
4238            poll_sme_req(&mut exec, &mut sme_fut),
4239            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => {
4240                responder.send().expect("could not send sme response");
4241            }
4242        );
4243
4244        // Run the future again and ensure that it has not exited after receiving the response.
4245        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4246
4247        // Absorb the connect request.
4248        let connect_txn_handle = assert_matches!(
4249            poll_sme_req(&mut exec, &mut sme_fut),
4250            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => {
4251                // Send connection response.
4252                let (_stream, ctrl) = txn.expect("connect txn unused")
4253                    .into_stream_and_control_handle();
4254                ctrl
4255            }
4256        );
4257        connect_txn_handle
4258            .send_on_connect_result(&fake_successful_connect_result())
4259            .expect("failed to send connection completion");
4260        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4261
4262        // Verify roam monitor request was sent.
4263        assert_matches!(test_values.roam_service_request_receiver.try_recv(), Ok(request) => {
4264            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
4265        });
4266
4267        // Run the state machine
4268        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4269
4270        // Send a disconnect request
4271        let mut client = Client::new(client_req_sender);
4272        let (sender, mut receiver) = oneshot::channel();
4273        client
4274            .disconnect(
4275                PolicyDisconnectionMigratedMetricDimensionReason::NetworkConfigUpdated,
4276                sender,
4277            )
4278            .expect("failed to make request");
4279
4280        // Run the state machine so that it handles the disconnect message.
4281        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4282        assert_matches!(
4283            poll_sme_req(&mut exec, &mut sme_fut),
4284            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::NetworkConfigUpdated }) => {
4285                responder.send().expect("could not send sme response");
4286            }
4287        );
4288
4289        // The state machine should exit following the disconnect request.
4290        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4291
4292        // Expect the responder to be acknowledged
4293        assert_matches!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
4294    }
4295
4296    #[fuchsia::test]
4297    fn serve_loop_handles_state_machine_error() {
4298        let mut exec = fasync::TestExecutor::new();
4299        let test_values = test_setup();
4300        let sme_proxy = test_values.common_options.proxy;
4301        let sme_event_stream = sme_proxy.take_event_stream();
4302        let (_client_req_sender, client_req_stream) = mpsc::channel(1);
4303
4304        // Set the status to something non-disconnected so that we can verify that the state is set
4305        // when the state machine exits.
4306        test_values.common_options.status_publisher.publish_status(Status::Connecting);
4307
4308        // Create a connect request so that the state machine does not immediately exit.
4309        let connect_selection = generate_connect_selection();
4310
4311        let fut = serve(
4312            0,
4313            sme_proxy,
4314            sme_event_stream,
4315            client_req_stream,
4316            test_values.common_options.update_sender,
4317            test_values.common_options.saved_networks_manager,
4318            Some(connect_selection),
4319            test_values.common_options.telemetry_sender,
4320            test_values.common_options.defect_sender,
4321            test_values.common_options.roam_manager,
4322            test_values.common_options.status_publisher.clone(),
4323        );
4324        let mut fut = pin!(fut);
4325
4326        // Drop the server end of the SME stream, so it causes an error
4327        drop(test_values.sme_req_stream);
4328
4329        // Ensure the state machine exits
4330        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4331
4332        // Verify that the state has been set to disconnected.
4333        let status = test_values.status_reader.read_status().expect("could not get reader");
4334        assert_eq!(status, Status::Disconnected);
4335    }
4336
4337    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
4338        fidl_sme::ConnectResult {
4339            code: fidl_ieee80211::StatusCode::Success,
4340            is_credential_rejected: false,
4341            is_reconnect: false,
4342        }
4343    }
4344
4345    #[fuchsia::test]
4346    fn disconnecting_sets_status() {
4347        let mut exec = fasync::TestExecutor::new();
4348        let test_values = test_setup();
4349
4350        let (sender, _) = oneshot::channel();
4351        let disconnecting_options = DisconnectingOptions {
4352            disconnect_responder: Some(sender),
4353            previous_network: None,
4354            next_network: None,
4355            reason: types::DisconnectReason::RegulatoryRegionChange,
4356        };
4357
4358        // Run the disconnecting state machine.
4359        let initial_state = disconnecting_state(test_values.common_options, disconnecting_options);
4360        let fut = run_state_machine(initial_state);
4361        let mut fut = pin!(fut);
4362        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4363
4364        // Verify the disconnecting state has been reported.
4365        let status = test_values.status_reader.read_status().expect("could not get reader");
4366        assert_eq!(status, Status::Disconnecting);
4367    }
4368
4369    #[fuchsia::test]
4370    fn connecting_sets_status() {
4371        let mut exec = fasync::TestExecutor::new_with_fake_time();
4372        let connection_attempt_time = fasync::MonotonicInstant::from_nanos(0);
4373        exec.set_fake_time(connection_attempt_time);
4374        let test_values = test_setup();
4375
4376        let connect_selection = generate_connect_selection();
4377        let connecting_options =
4378            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
4379
4380        // Run the connecting state
4381        let initial_state = connecting_state(test_values.common_options, connecting_options);
4382        let fut = run_state_machine(initial_state);
4383        let mut fut = pin!(fut);
4384        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4385
4386        // Verify the status was set.
4387        let status = test_values.status_reader.read_status().expect("failed to read status");
4388        assert_matches!(status, Status::Connecting);
4389    }
4390
4391    struct InspectTestValues {
4392        exec: fasync::TestExecutor,
4393        inspector: fuchsia_inspect::Inspector,
4394        _node: fuchsia_inspect::Node,
4395        status_node: fuchsia_inspect_contrib::nodes::BoundedListNode,
4396    }
4397
4398    impl InspectTestValues {
4399        fn new(exec: fasync::TestExecutor) -> Self {
4400            let inspector = fuchsia_inspect::Inspector::default();
4401            let _node = inspector.root().create_child("node");
4402            let status_node =
4403                fuchsia_inspect_contrib::nodes::BoundedListNode::new(_node.clone_weak(), 1);
4404
4405            Self { exec, inspector, _node, status_node }
4406        }
4407
4408        fn log_status(&mut self, status: Status) -> fuchsia_inspect::reader::DiagnosticsHierarchy {
4409            fuchsia_inspect_contrib::inspect_log!(self.status_node, "status" => status);
4410            let read_fut = fuchsia_inspect::reader::read(&self.inspector);
4411            let mut read_fut = pin!(read_fut);
4412            assert_matches!(
4413                self.exec.run_until_stalled(&mut read_fut),
4414                Poll::Ready(Ok(hierarchy)) => hierarchy
4415            )
4416        }
4417    }
4418
4419    #[fuchsia::test]
4420    fn test_disconnecting_status_inspect_log() {
4421        let exec = fasync::TestExecutor::new_with_fake_time();
4422        let mut test_values = InspectTestValues::new(exec);
4423        let hierarchy = test_values.log_status(Status::Disconnecting);
4424        diagnostics_assertions::assert_data_tree!(
4425            @executor test_values.exec,
4426            hierarchy,
4427            root: contains {
4428                node: contains {
4429                    "0": contains {
4430                        status: "Disconnecting"
4431                    }
4432                }
4433        });
4434    }
4435
4436    #[fuchsia::test]
4437    fn test_disconnected_status_inspect_log() {
4438        let exec = fasync::TestExecutor::new_with_fake_time();
4439        let mut test_values = InspectTestValues::new(exec);
4440        let hierarchy = test_values.log_status(Status::Disconnected);
4441        diagnostics_assertions::assert_data_tree!(
4442            @executor test_values.exec,
4443            hierarchy,
4444            root: contains {
4445                node: contains {
4446                    "0": contains {
4447                        status: "Disconnected"
4448                    }
4449                }
4450        });
4451    }
4452
4453    #[fuchsia::test]
4454    fn test_connecting_status_inspect_log() {
4455        let exec = fasync::TestExecutor::new_with_fake_time();
4456        let mut test_values = InspectTestValues::new(exec);
4457        let hierarchy = test_values.log_status(Status::Connecting);
4458        diagnostics_assertions::assert_data_tree!(
4459            @executor test_values.exec,
4460            hierarchy,
4461            root: contains {
4462                node: contains {
4463                    "0": contains {
4464                        status: "Connecting"
4465                    }
4466                }
4467        });
4468    }
4469
4470    #[fuchsia::test]
4471    fn test_connected_status_inspect_log() {
4472        let exec = fasync::TestExecutor::new_with_fake_time();
4473        let mut test_values = InspectTestValues::new(exec);
4474        let hierarchy = test_values.log_status(Status::Connected { channel: 1, rssi: 2, snr: 3 });
4475        diagnostics_assertions::assert_data_tree!(
4476            @executor test_values.exec,
4477            hierarchy,
4478            root: contains {
4479                node: contains {
4480                    "0": contains {
4481                        status: contains {
4482                            Connected: { channel: 1_u64, rssi: 2_i64, snr: 3_i64 }
4483                        }
4484                    }
4485                }
4486        });
4487    }
4488}