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::historical_list::HistoricalList;
17use crate::util::listener::Message::NotifyListeners;
18use crate::util::listener::{ClientListenerMessageSender, ClientNetworkState, ClientStateUpdate};
19use crate::util::state_machine::{self, ExitReason, IntoStateExt, StateMachineStatusPublisher};
20use anyhow::format_err;
21use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
22use fidl_fuchsia_wlan_internal as fidl_internal;
23use fidl_fuchsia_wlan_policy as fidl_policy;
24use fidl_fuchsia_wlan_sme as fidl_sme;
25use fuchsia_async::{self as fasync, DurationExt};
26use fuchsia_inspect::Node as InspectNode;
27use fuchsia_inspect_contrib::inspect_insert;
28use fuchsia_inspect_contrib::log::WriteInspect;
29use futures::channel::{mpsc, oneshot};
30use futures::future::{Fuse, FutureExt};
31use futures::select;
32use futures::stream::{self, StreamExt, TryStreamExt};
33use log::{debug, error, info, warn};
34use std::borrow::Cow;
35use std::pin::Pin;
36use std::sync::Arc;
37use wlan_common::bss::BssDescription;
38use wlan_common::channel::{Cbw, Channel};
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>>;
47
48#[derive(Clone)]
49struct PendingRoam {
50    pub request: PolicyRoamRequest,
51    pub timestamp: fasync::MonotonicInstant,
52}
53impl From<PolicyRoamRequest> for PendingRoam {
54    fn from(request: PolicyRoamRequest) -> Self {
55        Self { request, timestamp: fasync::MonotonicInstant::now() }
56    }
57}
58
59pub trait ClientApi {
60    fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error>;
61    fn disconnect(
62        &mut self,
63        reason: types::DisconnectReason,
64        responder: oneshot::Sender<()>,
65    ) -> Result<(), anyhow::Error>;
66
67    /// Queries the liveness of the channel used to control the client state machine.  If the
68    /// channel is not alive, this indicates that the client state machine has exited.
69    fn is_alive(&self) -> bool;
70}
71
72pub struct Client {
73    req_sender: mpsc::Sender<ManualRequest>,
74}
75
76impl Client {
77    pub fn new(req_sender: mpsc::Sender<ManualRequest>) -> Self {
78        Self { req_sender }
79    }
80}
81
82impl ClientApi for Client {
83    fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error> {
84        self.req_sender
85            .try_send(ManualRequest::Connect(Box::new(selection)))
86            .map_err(|e| format_err!("failed to send connect selection: {:?}", e))
87    }
88
89    fn disconnect(
90        &mut self,
91        reason: types::DisconnectReason,
92        responder: oneshot::Sender<()>,
93    ) -> Result<(), anyhow::Error> {
94        self.req_sender
95            .try_send(ManualRequest::Disconnect((reason, responder)))
96            .map_err(|e| format_err!("failed to send disconnect request: {:?}", e))
97    }
98
99    fn is_alive(&self) -> bool {
100        !self.req_sender.is_closed()
101    }
102}
103
104// TODO(https://fxbug.dev/324167674): fix.
105pub enum ManualRequest {
106    Connect(Box<types::ConnectSelection>),
107    Disconnect((types::DisconnectReason, oneshot::Sender<()>)),
108}
109
110#[derive(Clone, Debug, Default, PartialEq)]
111pub enum Status {
112    Disconnecting,
113    #[default]
114    Disconnected,
115    Connecting,
116    Connected {
117        channel: u8,
118        rssi: i8,
119        snr: i8,
120    },
121}
122
123impl Status {
124    fn from_ap_state(ap_state: &types::ApState) -> Self {
125        Status::Connected {
126            channel: ap_state.tracked.channel.primary,
127            rssi: ap_state.tracked.signal.rssi_dbm,
128            snr: ap_state.tracked.signal.snr_db,
129        }
130    }
131}
132
133impl WriteInspect for Status {
134    fn write_inspect<'a>(&self, writer: &InspectNode, key: impl Into<Cow<'a, str>>) {
135        match self {
136            Status::Connected { channel, rssi, snr } => {
137                inspect_insert!(writer, var key: {
138                    Connected: {
139                        channel: channel,
140                        rssi: rssi,
141                        snr: snr
142                    }
143                })
144            }
145            other => inspect_insert!(writer, var key: format!("{:?}", other)),
146        }
147    }
148}
149
150fn send_listener_state_update(
151    sender: &ClientListenerMessageSender,
152    network_update: Option<ClientNetworkState>,
153) {
154    let mut networks = vec![];
155    if let Some(network) = network_update {
156        networks.push(network)
157    }
158
159    let updates =
160        ClientStateUpdate { state: fidl_policy::WlanClientState::ConnectionsEnabled, networks };
161    match sender.clone().unbounded_send(NotifyListeners(updates)) {
162        Ok(_) => (),
163        Err(e) => error!("failed to send state update: {:?}", e),
164    };
165}
166
167pub async fn serve(
168    iface_id: u16,
169    proxy: SmeForClientStateMachine,
170    sme_event_stream: fidl_sme::ClientSmeEventStream,
171    req_stream: mpsc::Receiver<ManualRequest>,
172    update_sender: ClientListenerMessageSender,
173    saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
174    connect_selection: Option<types::ConnectSelection>,
175    telemetry_sender: TelemetrySender,
176    defect_sender: mpsc::Sender<Defect>,
177    roam_manager: RoamManager,
178    status_publisher: StateMachineStatusPublisher<Status>,
179) {
180    let next_network = connect_selection
181        .map(|selection| ConnectingOptions { connect_selection: selection, attempt_counter: 0 });
182    let disconnect_options = DisconnectingOptions {
183        disconnect_responder: None,
184        previous_network: None,
185        next_network,
186        reason: types::DisconnectReason::Startup,
187    };
188    let common_options = CommonStateOptions {
189        proxy,
190        req_stream: req_stream.fuse(),
191        update_sender,
192        saved_networks_manager,
193        telemetry_sender: telemetry_sender.clone(),
194        iface_id,
195        defect_sender,
196        roam_manager,
197        status_publisher: status_publisher.clone(),
198    };
199    let state_machine =
200        disconnecting_state(common_options, disconnect_options).into_state_machine();
201    let removal_watcher = sme_event_stream.map_ok(|_| ()).try_collect::<()>();
202    select! {
203        state_machine = state_machine.fuse() => {
204            match state_machine {
205                Err(ExitReason(Err(e))) => error!("Client state machine for iface #{} terminated with an error: {:?}",
206                    iface_id, e),
207                Err(ExitReason(Ok(_))) => info!("Client state machine for iface #{} exited gracefully",
208                    iface_id,),
209            }
210        }
211        removal_watcher = removal_watcher.fuse() => {
212            match removal_watcher {
213                Ok(()) => {
214                    info!("Device was unexpectedly removed.");
215                }
216                Err(e) => {
217                    info!("Error reading from Client SME channel of iface #{}: {:?}",
218                        iface_id, e);
219                }
220            }
221
222            telemetry_sender
223                .send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info: None });
224        },
225    }
226
227    status_publisher.publish_status(Status::Disconnected);
228}
229
230/// Common parameters passed to all states
231struct CommonStateOptions {
232    proxy: SmeForClientStateMachine,
233    req_stream: ReqStream,
234    update_sender: ClientListenerMessageSender,
235    saved_networks_manager: Arc<dyn SavedNetworksManagerApi>,
236    telemetry_sender: TelemetrySender,
237    iface_id: u16,
238    defect_sender: mpsc::Sender<Defect>,
239    roam_manager: RoamManager,
240    status_publisher: StateMachineStatusPublisher<Status>,
241}
242
243impl CommonStateOptions {
244    async fn network_is_likely_hidden(&self, options: &ConnectingOptions) -> bool {
245        match self
246            .saved_networks_manager
247            .lookup(&options.connect_selection.target.network)
248            .await
249            .iter()
250            .find(|&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: HistoricalList<types::TimestampedSignal>,
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 = HistoricalList::new(NUM_PAST_SCORES);
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: HistoricalList::new(NUM_PAST_SCORES),
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 Cbw::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.cbw
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 = HistoricalList::new(NUM_PAST_SCORES);
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::{PastConnectionList, network_config};
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 rand::Rng;
1258    use std::pin::pin;
1259    use wlan_common::random_fidl_bss_description;
1260    use wlan_metrics_registry::PolicyDisconnectionMigratedMetricDimensionReason;
1261
1262    struct TestValues {
1263        common_options: CommonStateOptions,
1264        sme_req_stream: fidl_sme::ClientSmeRequestStream,
1265        saved_networks_manager: Arc<FakeSavedNetworksManager>,
1266        client_req_sender: mpsc::Sender<ManualRequest>,
1267        update_receiver: mpsc::UnboundedReceiver<listener::ClientListenerMessage>,
1268        telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
1269        defect_receiver: mpsc::Receiver<Defect>,
1270        roam_service_request_receiver: mpsc::Receiver<RoamServiceRequest>,
1271        status_reader: StateMachineStatusReader<Status>,
1272    }
1273
1274    fn test_setup() -> TestValues {
1275        let (client_req_sender, client_req_stream) = mpsc::channel(1);
1276        let (update_sender, update_receiver) = mpsc::unbounded();
1277        let (sme_proxy, sme_server) = create_proxy::<fidl_sme::ClientSmeMarker>();
1278        let sme_req_stream = sme_server.into_stream();
1279        let saved_networks = FakeSavedNetworksManager::new();
1280        let saved_networks_manager = Arc::new(saved_networks);
1281        let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1282        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1283        let (defect_sender, defect_receiver) = mpsc::channel(100);
1284        let (roam_service_request_sender, roam_service_request_receiver) = mpsc::channel(100);
1285        let roam_manager = RoamManager::new(roam_service_request_sender);
1286        let (status_publisher, status_reader) = status_publisher_and_reader::<Status>();
1287
1288        TestValues {
1289            common_options: CommonStateOptions {
1290                proxy: SmeForClientStateMachine::new(sme_proxy, 0, defect_sender.clone()),
1291                req_stream: client_req_stream.fuse(),
1292                update_sender,
1293                saved_networks_manager: saved_networks_manager.clone(),
1294                telemetry_sender,
1295                iface_id: 1,
1296                defect_sender,
1297                roam_manager,
1298                status_publisher,
1299            },
1300            sme_req_stream,
1301            saved_networks_manager,
1302            client_req_sender,
1303            update_receiver,
1304            telemetry_receiver,
1305            defect_receiver,
1306            roam_service_request_receiver,
1307            status_reader,
1308        }
1309    }
1310
1311    #[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
1312    async fn run_state_machine(fut: impl Future<Output = Result<State, ExitReason>> + 'static) {
1313        let state_machine = fut.into_state_machine();
1314        select! {
1315            _state_machine = state_machine.fuse() => return,
1316        }
1317    }
1318
1319    #[fuchsia::test]
1320    fn connecting_state_successfully_connects() {
1321        let mut exec = fasync::TestExecutor::new();
1322        let mut test_values = test_setup();
1323
1324        let connect_selection = generate_connect_selection();
1325        let bss_description =
1326            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1327
1328        // Store the network in the saved_networks_manager, so we can record connection success
1329        let save_fut = test_values.saved_networks_manager.store(
1330            connect_selection.target.network.clone(),
1331            connect_selection.target.credential.clone(),
1332        );
1333        let mut save_fut = pin!(save_fut);
1334        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
1335
1336        // Check that the saved networks manager has the expected initial data
1337        let saved_networks = exec.run_singlethreaded(
1338            test_values.saved_networks_manager.lookup(&connect_selection.target.network.clone()),
1339        );
1340        assert!(!saved_networks[0].has_ever_connected);
1341        assert!(saved_networks[0].hidden_probability > 0.0);
1342
1343        let connecting_options =
1344            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1345        let initial_state = connecting_state(test_values.common_options, connecting_options);
1346        let fut = run_state_machine(initial_state);
1347        let mut fut = pin!(fut);
1348        let sme_fut = test_values.sme_req_stream.into_future();
1349        let mut sme_fut = pin!(sme_fut);
1350
1351        // Run the state machine
1352        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1353
1354        // Ensure a connect request is sent to the SME
1355        let connect_txn_handle = assert_matches!(
1356            poll_sme_req(&mut exec, &mut sme_fut),
1357            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1358                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1359                assert_eq!(req.bss_description, bss_description);
1360                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1361                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1362                // Send connection response.
1363                let (_stream, ctrl) = txn.expect("connect txn unused")
1364                    .into_stream_and_control_handle();
1365                ctrl
1366            }
1367        );
1368        connect_txn_handle
1369            .send_on_connect_result(&fake_successful_connect_result())
1370            .expect("failed to send connection completion");
1371
1372        // Check for a connecting update
1373        let client_state_update = ClientStateUpdate {
1374            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1375            networks: vec![ClientNetworkState {
1376                id: connect_selection.target.network.clone(),
1377                state: fidl_policy::ConnectionState::Connecting,
1378                status: None,
1379            }],
1380        };
1381        assert_matches!(
1382            test_values.update_receiver.try_next(),
1383            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1384            assert_eq!(updates, client_state_update);
1385        });
1386
1387        // Progress the state machine
1388        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1389
1390        // Check for a connect update
1391        let client_state_update = ClientStateUpdate {
1392            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1393            networks: vec![ClientNetworkState {
1394                id: connect_selection.target.network.clone(),
1395                state: fidl_policy::ConnectionState::Connected,
1396                status: None,
1397            }],
1398        };
1399        assert_matches!(
1400            test_values.update_receiver.try_next(),
1401            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1402            assert_eq!(updates, client_state_update);
1403        });
1404
1405        // Check that the connection was recorded to SavedNetworksManager
1406        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1407            let expected_connect_result = ConnectResultRecord {
1408                 id: connect_selection.target.network.clone(),
1409                 credential: connect_selection.target.credential.clone(),
1410                 bssid: types::Bssid::from(bss_description.bssid),
1411                 connect_result: fake_successful_connect_result(),
1412                 scan_type: connect_selection.target.bss.observation,
1413            };
1414            assert_eq!(data, &expected_connect_result);
1415        });
1416
1417        // Progress the state machine
1418        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1419
1420        // Ensure no further updates were sent to listeners
1421        assert_matches!(
1422            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1423            Poll::Pending
1424        );
1425    }
1426
1427    #[fuchsia::test]
1428    fn connecting_state_times_out() {
1429        let mut exec = fasync::TestExecutor::new();
1430        let mut test_values = test_setup();
1431
1432        let connect_selection = generate_connect_selection();
1433        let bss_description =
1434            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1435
1436        // Store the network in the saved_networks_manager
1437        let save_fut = test_values.saved_networks_manager.store(
1438            connect_selection.target.network.clone(),
1439            connect_selection.target.credential.clone(),
1440        );
1441        let mut save_fut = pin!(save_fut);
1442        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
1443
1444        // Prepare state machine
1445        let connecting_options =
1446            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1447        let initial_state = connecting_state(test_values.common_options, connecting_options);
1448        let fut = run_state_machine(initial_state);
1449        let mut fut = pin!(fut);
1450        let sme_fut = test_values.sme_req_stream.into_future();
1451        let mut sme_fut = pin!(sme_fut);
1452
1453        // Run the state machine
1454        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1455
1456        // Ensure a connect request is sent to the SME
1457        let connect_txn_handle = assert_matches!(
1458            poll_sme_req(&mut exec, &mut sme_fut),
1459            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1460                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1461                assert_eq!(req.bss_description, bss_description);
1462                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1463                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1464                let (_stream, ctrl) = txn.expect("connect txn unused")
1465                    .into_stream_and_control_handle();
1466                ctrl
1467            }
1468        );
1469
1470        // Check for a connecting update
1471        let client_state_update = ClientStateUpdate {
1472            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1473            networks: vec![ClientNetworkState {
1474                id: connect_selection.target.network.clone(),
1475                state: fidl_policy::ConnectionState::Connecting,
1476                status: None,
1477            }],
1478        };
1479        assert_matches!(
1480            test_values.update_receiver.try_next(),
1481            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1482            assert_eq!(updates, client_state_update);
1483        });
1484
1485        // Respond with a SignalReport, which should not unblock connecting_state
1486        connect_txn_handle
1487            .send_on_signal_report(&fidl_internal::SignalReportIndication {
1488                rssi_dbm: -25,
1489                snr_db: 30,
1490            })
1491            .expect("failed to send singal report");
1492
1493        // Run the state machine. Should still be pending
1494        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1495
1496        // Wake up the next timer, which is the timeout for the connect request.
1497        assert!(exec.wake_next_timer().is_some());
1498
1499        // State machine should exit.
1500        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1501    }
1502
1503    #[fuchsia::test]
1504    fn connecting_state_successfully_scans_and_connects() {
1505        let mut exec = fasync::TestExecutor::new_with_fake_time();
1506        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(123));
1507        let mut test_values = test_setup();
1508
1509        let connect_selection = generate_connect_selection();
1510        let bss_description =
1511            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1512
1513        // Set how the SavedNetworksManager should respond to lookup_compatible for the scan.
1514        let expected_config = network_config::NetworkConfig::new(
1515            connect_selection.target.network.clone(),
1516            connect_selection.target.credential.clone(),
1517            connect_selection.target.saved_network_info.has_ever_connected,
1518            None,
1519        )
1520        .expect("failed to create network config");
1521        test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
1522
1523        let connecting_options =
1524            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1525        let initial_state = connecting_state(test_values.common_options, connecting_options);
1526        let fut = run_state_machine(initial_state);
1527        let mut fut = pin!(fut);
1528
1529        // Run the state machine
1530        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1531
1532        // Ensure a connect request is sent to the SME
1533        let sme_fut = test_values.sme_req_stream.into_future();
1534        let mut sme_fut = pin!(sme_fut);
1535        let time_to_connect = zx::MonotonicDuration::from_seconds(30);
1536        let connect_txn_handle = assert_matches!(
1537            poll_sme_req(&mut exec, &mut sme_fut),
1538            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1539                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1540                assert_eq!(req.bss_description, bss_description.clone());
1541                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1542                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1543                // Send connection response.
1544                exec.set_fake_time(fasync::MonotonicInstant::after(time_to_connect));
1545                let (_stream, ctrl) = txn.expect("connect txn unused")
1546                    .into_stream_and_control_handle();
1547                ctrl
1548            }
1549        );
1550        connect_txn_handle
1551            .send_on_connect_result(&fake_successful_connect_result())
1552            .expect("failed to send connection completion");
1553
1554        // Check for a connecting update
1555        let client_state_update = ClientStateUpdate {
1556            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1557            networks: vec![ClientNetworkState {
1558                id: connect_selection.target.network.clone(),
1559                state: fidl_policy::ConnectionState::Connecting,
1560                status: None,
1561            }],
1562        };
1563        assert_matches!(
1564            test_values.update_receiver.try_next(),
1565            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1566            assert_eq!(updates, client_state_update);
1567        });
1568
1569        // Progress the state machine
1570        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1571
1572        // Check for a connect update
1573        let client_state_update = ClientStateUpdate {
1574            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1575            networks: vec![ClientNetworkState {
1576                id: connect_selection.target.network.clone(),
1577                state: fidl_policy::ConnectionState::Connected,
1578                status: None,
1579            }],
1580        };
1581        assert_matches!(
1582            test_values.update_receiver.try_next(),
1583            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1584            assert_eq!(updates, client_state_update);
1585        });
1586
1587        // Check that the saved networks manager has the connection result recorded
1588        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1589            let expected_connect_result = ConnectResultRecord {
1590                 id: connect_selection.target.network.clone(),
1591                 credential: connect_selection.target.credential.clone(),
1592                 bssid: types::Bssid::from(bss_description.bssid),
1593                 connect_result: fake_successful_connect_result(),
1594                 scan_type: connect_selection.target.bss.observation,
1595            };
1596            assert_eq!(data, &expected_connect_result);
1597        });
1598
1599        // Check that connected telemetry event is sent
1600        assert_matches!(
1601            test_values.telemetry_receiver.try_next(),
1602            Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ })) => {
1603                assert_eq!(bss_description, ap_state.original().clone().into());
1604                assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1605                assert_eq!(policy_connect_reason, Some(connect_selection.reason));
1606                assert_eq!(result, fake_successful_connect_result());
1607            }
1608        );
1609
1610        // Progress the state machine
1611        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1612
1613        // Ensure no further updates were sent to listeners
1614        assert_matches!(
1615            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1616            Poll::Pending
1617        );
1618
1619        // Verify the Connected status was set.
1620        let status = test_values.status_reader.read_status().expect("failed to read status");
1621        assert_matches!(status, Status::Connected { .. });
1622
1623        // Send a disconnect and check that the connection data is correctly recorded
1624        let is_sme_reconnecting = false;
1625        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
1626        connect_txn_handle
1627            .send_on_disconnect(&fidl_disconnect_info)
1628            .expect("failed to send disconnection event");
1629        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1630
1631        // Verify roam monitor request was sent.
1632        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
1633            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
1634        });
1635
1636        // Run the state machine
1637        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1638
1639        let expected_recorded_connection = ConnectionRecord {
1640            id: connect_selection.target.network.clone(),
1641            credential: connect_selection.target.credential.clone(),
1642            data: PastConnectionData {
1643                bssid: types::Bssid::from(bss_description.bssid),
1644                disconnect_time: fasync::MonotonicInstant::now(),
1645                connection_uptime: zx::MonotonicDuration::from_minutes(0),
1646                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
1647                signal_at_disconnect: types::Signal {
1648                    rssi_dbm: bss_description.rssi_dbm,
1649                    snr_db: bss_description.snr_db,
1650                },
1651                // TODO: record average phy rate over connection once available
1652                average_tx_rate: 0,
1653            },
1654        };
1655        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [data] => {
1656            assert_eq!(data, &expected_recorded_connection);
1657        });
1658    }
1659
1660    #[fuchsia::test]
1661    fn connecting_state_fails_to_connect_and_retries() {
1662        let mut exec = fasync::TestExecutor::new();
1663        let mut test_values = test_setup();
1664
1665        let connect_selection = generate_connect_selection();
1666        let bss_description =
1667            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1668
1669        let connecting_options =
1670            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
1671        let initial_state = connecting_state(test_values.common_options, connecting_options);
1672        let fut = run_state_machine(initial_state);
1673        let mut fut = pin!(fut);
1674        let sme_fut = test_values.sme_req_stream.into_future();
1675        let mut sme_fut = pin!(sme_fut);
1676
1677        // Run the state machine
1678        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1679
1680        // Ensure a connect request is sent to the SME
1681        let mut connect_txn_handle = assert_matches!(
1682            poll_sme_req(&mut exec, &mut sme_fut),
1683            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1684                assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec());
1685                 // Send connection response.
1686                let (_stream, ctrl) = txn.expect("connect txn unused")
1687                    .into_stream_and_control_handle();
1688                ctrl
1689            }
1690        );
1691        let connect_result = fidl_sme::ConnectResult {
1692            code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1693            ..fake_successful_connect_result()
1694        };
1695        connect_txn_handle
1696            .send_on_connect_result(&connect_result)
1697            .expect("failed to send connection completion");
1698
1699        // Check for a connecting update
1700        let client_state_update = ClientStateUpdate {
1701            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1702            networks: vec![ClientNetworkState {
1703                id: types::NetworkIdentifier {
1704                    ssid: connect_selection.target.network.ssid.clone(),
1705                    security_type: types::SecurityType::Wpa2,
1706                },
1707                state: fidl_policy::ConnectionState::Connecting,
1708                status: None,
1709            }],
1710        };
1711        assert_matches!(
1712            test_values.update_receiver.try_next(),
1713            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1714            assert_eq!(updates, client_state_update);
1715        });
1716
1717        // Progress the state machine
1718        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1719        assert!(exec.wake_next_timer().is_some());
1720        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1721
1722        // Check that connect result telemetry event is sent
1723        assert_matches!(
1724            test_values.telemetry_receiver.try_next(),
1725            Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ })) => {
1726                assert_eq!(bss_description, ap_state.original().clone().into());
1727                assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1728                assert_eq!(policy_connect_reason, Some(connect_selection.reason));
1729                assert_eq!(result, connect_result);
1730            }
1731        );
1732
1733        // Ensure a disconnect request is sent to the SME
1734        assert_matches!(
1735            poll_sme_req(&mut exec, &mut sme_fut),
1736            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FailedToConnect }) => {
1737                responder.send().expect("could not send sme response");
1738            }
1739        );
1740
1741        // Progress the state machine
1742        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1743
1744        // Ensure a connect request is sent to the SME
1745        connect_txn_handle = assert_matches!(
1746            poll_sme_req(&mut exec, &mut sme_fut),
1747            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1748                assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec());
1749                assert_eq!(req.bss_description, Sequestered::release(connect_selection.target.bss.bss_description));
1750                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1751                 // Send connection response.
1752                let (_stream, ctrl) = txn.expect("connect txn unused")
1753                    .into_stream_and_control_handle();
1754                ctrl
1755            }
1756        );
1757        let connect_result = fake_successful_connect_result();
1758        connect_txn_handle
1759            .send_on_connect_result(&connect_result)
1760            .expect("failed to send connection completion");
1761
1762        // Progress the state machine
1763        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1764
1765        // Empty update sent to NotifyListeners (which in this case, will not actually be sent.)
1766        assert_matches!(
1767            test_values.update_receiver.try_next(),
1768            Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate {
1769                state: fidl_policy::WlanClientState::ConnectionsEnabled,
1770                networks
1771            }))) => {
1772                assert!(networks.is_empty());
1773            }
1774        );
1775
1776        // A defect should be logged.
1777        assert_matches!(
1778            test_values.defect_receiver.try_next(),
1779            Ok(Some(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 })))
1780        );
1781
1782        // Check for a connected update
1783        let client_state_update = ClientStateUpdate {
1784            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1785            networks: vec![ClientNetworkState {
1786                id: types::NetworkIdentifier {
1787                    ssid: connect_selection.target.network.ssid.clone(),
1788                    security_type: types::SecurityType::Wpa2,
1789                },
1790                state: fidl_policy::ConnectionState::Connected,
1791                status: None,
1792            }],
1793        };
1794        assert_matches!(
1795            test_values.update_receiver.try_next(),
1796            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1797            assert_eq!(updates, client_state_update);
1798        });
1799
1800        // Progress the state machine
1801        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1802
1803        // Ensure no further updates were sent to listeners
1804        assert_matches!(
1805            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
1806            Poll::Pending
1807        );
1808    }
1809
1810    #[fuchsia::test]
1811    fn connecting_state_fails_to_connect_at_max_retries() {
1812        let mut exec = fasync::TestExecutor::new();
1813        let mut test_values = test_setup();
1814
1815        let connect_selection = generate_connect_selection();
1816        let bss_description =
1817            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1818
1819        // save network to check that failed connect is recorded
1820        assert!(
1821            exec.run_singlethreaded(test_values.saved_networks_manager.store(
1822                connect_selection.target.network.clone(),
1823                connect_selection.target.credential.clone()
1824            ),)
1825                .expect("Failed to save network")
1826                .is_none()
1827        );
1828
1829        let connecting_options = ConnectingOptions {
1830            connect_selection: connect_selection.clone(),
1831            attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
1832        };
1833        let initial_state = connecting_state(test_values.common_options, connecting_options);
1834        let fut = run_state_machine(initial_state);
1835        let mut fut = pin!(fut);
1836        let sme_fut = test_values.sme_req_stream.into_future();
1837        let mut sme_fut = pin!(sme_fut);
1838
1839        // Run the state machine
1840        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1841
1842        // Ensure a connect request is sent to the SME
1843        assert_matches!(
1844            poll_sme_req(&mut exec, &mut sme_fut),
1845            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1846                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1847                assert_eq!(req.bss_description, bss_description.clone());
1848                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1849                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1850                 // Send connection response.
1851                let (_stream, ctrl) = txn.expect("connect txn unused")
1852                    .into_stream_and_control_handle();
1853                let connect_result = fidl_sme::ConnectResult {
1854                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1855                    ..fake_successful_connect_result()
1856                };
1857                ctrl
1858                    .send_on_connect_result(&connect_result)
1859                    .expect("failed to send connection completion");
1860            }
1861        );
1862
1863        // After failing to reconnect, the state machine should exit so that the state machine
1864        // monitor can attempt to reconnect the interface.
1865        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1866
1867        // Check for a connect update
1868        let client_state_update = ClientStateUpdate {
1869            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1870            networks: vec![ClientNetworkState {
1871                id: connect_selection.target.network.clone(),
1872                state: fidl_policy::ConnectionState::Failed,
1873                status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
1874            }],
1875        };
1876        assert_matches!(
1877            test_values.update_receiver.try_next(),
1878            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1879            assert_eq!(updates, client_state_update);
1880        });
1881
1882        // Check that failure was recorded in SavedNetworksManager
1883        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1884            let connect_result = fidl_sme::ConnectResult {
1885                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1886                is_credential_rejected: false,
1887                is_reconnect: false,
1888            };
1889            let expected_connect_result = ConnectResultRecord {
1890                 id: connect_selection.target.network.clone(),
1891                 credential: connect_selection.target.credential.clone(),
1892                 bssid: types::Bssid::from(bss_description.bssid),
1893                 connect_result,
1894                 scan_type: connect_selection.target.bss.observation,
1895            };
1896            assert_eq!(data, &expected_connect_result);
1897        });
1898
1899        // A defect should be logged.
1900        assert_matches!(
1901            test_values.defect_receiver.try_next(),
1902            Ok(Some(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 })))
1903        );
1904    }
1905
1906    #[fuchsia::test]
1907    fn connecting_state_fails_to_connect_with_bad_credentials() {
1908        let mut exec = fasync::TestExecutor::new();
1909        let mut test_values = test_setup();
1910
1911        let connect_selection = generate_connect_selection();
1912        let bss_description =
1913            Sequestered::release(connect_selection.target.bss.bss_description.clone());
1914
1915        assert!(
1916            exec.run_singlethreaded(test_values.saved_networks_manager.store(
1917                connect_selection.target.network.clone(),
1918                connect_selection.target.credential.clone()
1919            ),)
1920                .expect("Failed to save network")
1921                .is_none()
1922        );
1923
1924        let connecting_options = ConnectingOptions {
1925            connect_selection: connect_selection.clone(),
1926            attempt_counter: MAX_CONNECTION_ATTEMPTS - 1,
1927        };
1928        let initial_state = connecting_state(test_values.common_options, connecting_options);
1929        let fut = run_state_machine(initial_state);
1930        let mut fut = pin!(fut);
1931        let sme_fut = test_values.sme_req_stream.into_future();
1932        let mut sme_fut = pin!(sme_fut);
1933
1934        // Run the state machine
1935        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
1936
1937        // Ensure a connect request is sent to the SME
1938        assert_matches!(
1939            poll_sme_req(&mut exec, &mut sme_fut),
1940            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
1941                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
1942                assert_eq!(req.bss_description, bss_description.clone());
1943                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
1944                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
1945                 // Send connection response.
1946                let (_stream, ctrl) = txn.expect("connect txn unused")
1947                    .into_stream_and_control_handle();
1948                let connect_result = fidl_sme::ConnectResult {
1949                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1950                    is_credential_rejected: true,
1951                    ..fake_successful_connect_result()
1952                };
1953                ctrl
1954                    .send_on_connect_result(&connect_result)
1955                    .expect("failed to send connection completion");
1956            }
1957        );
1958
1959        // The state machine should exit when bad credentials are detected so that the state
1960        // machine monitor can try to connect to another network.
1961        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
1962
1963        // Check for a connect update
1964        let client_state_update = ClientStateUpdate {
1965            state: fidl_policy::WlanClientState::ConnectionsEnabled,
1966            networks: vec![ClientNetworkState {
1967                id: connect_selection.target.network.clone(),
1968                state: fidl_policy::ConnectionState::Failed,
1969                status: Some(fidl_policy::DisconnectStatus::CredentialsFailed),
1970            }],
1971        };
1972        assert_matches!(
1973            test_values.update_receiver.try_next(),
1974            Ok(Some(listener::Message::NotifyListeners(updates))) => {
1975            assert_eq!(updates, client_state_update);
1976        });
1977
1978        // Check that failure was recorded to SavedNetworksManager
1979        assert_matches!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => {
1980            let connect_result = fidl_sme::ConnectResult {
1981                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1982                is_credential_rejected: true,
1983                is_reconnect: false,
1984            };
1985            let expected_connect_result = ConnectResultRecord {
1986                 id: connect_selection.target.network.clone(),
1987                 credential: connect_selection.target.credential.clone(),
1988                 bssid: types::Bssid::from(bss_description.bssid),
1989                 connect_result,
1990                 scan_type: connect_selection.target.bss.observation,
1991            };
1992            assert_eq!(data, &expected_connect_result);
1993        });
1994
1995        // No defect should have been observed.
1996        assert_matches!(test_values.defect_receiver.try_next(), Ok(None));
1997    }
1998
1999    #[fuchsia::test]
2000    fn connecting_state_gets_duplicate_connect_selection() {
2001        let mut exec = fasync::TestExecutor::new();
2002        let mut test_values = test_setup();
2003
2004        let connect_selection = generate_connect_selection();
2005        let bss_description =
2006            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2007
2008        let connecting_options =
2009            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2010        let initial_state = connecting_state(test_values.common_options, connecting_options);
2011        let fut = run_state_machine(initial_state);
2012        let mut fut = pin!(fut);
2013        let sme_fut = test_values.sme_req_stream.into_future();
2014        let mut sme_fut = pin!(sme_fut);
2015
2016        // Run the state machine
2017        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2018
2019        // Check for a connecting update
2020        let client_state_update = ClientStateUpdate {
2021            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2022            networks: vec![ClientNetworkState {
2023                id: types::NetworkIdentifier {
2024                    ssid: connect_selection.target.network.ssid.clone(),
2025                    security_type: types::SecurityType::Wpa2,
2026                },
2027                state: fidl_policy::ConnectionState::Connecting,
2028                status: None,
2029            }],
2030        };
2031        assert_matches!(
2032            test_values.update_receiver.try_next(),
2033            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2034            assert_eq!(updates, client_state_update);
2035        });
2036
2037        // Send a duplicate connect request
2038        let mut client = Client::new(test_values.client_req_sender);
2039        let duplicate_request = types::ConnectSelection {
2040            // this incoming request should be deduped regardless of the reason
2041            reason: types::ConnectReason::ProactiveNetworkSwitch,
2042            ..connect_selection.clone()
2043        };
2044        client.connect(duplicate_request).expect("failed to make request");
2045
2046        // Progress the state machine
2047        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2048
2049        // Ensure a connect request is sent to the SME
2050        let connect_txn_handle = assert_matches!(
2051            poll_sme_req(&mut exec, &mut sme_fut),
2052            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
2053                assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec());
2054                assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active);
2055                assert_eq!(req.bss_description, bss_description);
2056                assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss);
2057                 // Send connection response.
2058                let (_stream, ctrl) = txn.expect("connect txn unused")
2059                    .into_stream_and_control_handle();
2060                ctrl
2061            }
2062        );
2063        connect_txn_handle
2064            .send_on_connect_result(&fake_successful_connect_result())
2065            .expect("failed to send connection completion");
2066
2067        // Progress the state machine
2068        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2069
2070        // Check for a connect update
2071        let client_state_update = ClientStateUpdate {
2072            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2073            networks: vec![ClientNetworkState {
2074                id: connect_selection.target.network.clone(),
2075                state: fidl_policy::ConnectionState::Connected,
2076                status: None,
2077            }],
2078        };
2079        assert_matches!(
2080            test_values.update_receiver.try_next(),
2081            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2082            assert_eq!(updates, client_state_update);
2083        });
2084
2085        // Progress the state machine
2086        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2087
2088        // Ensure no further updates were sent to listeners
2089        assert_matches!(
2090            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
2091            Poll::Pending
2092        );
2093    }
2094
2095    #[fuchsia::test]
2096    fn connecting_state_has_broken_sme() {
2097        let mut exec = fasync::TestExecutor::new();
2098        let test_values = test_setup();
2099
2100        let connect_selection = generate_connect_selection();
2101
2102        let connecting_options =
2103            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2104        let initial_state = connecting_state(test_values.common_options, connecting_options);
2105        let fut = run_state_machine(initial_state);
2106        let mut fut = pin!(fut);
2107
2108        // Break the SME by dropping the server end of the SME stream, so it causes an error
2109        drop(test_values.sme_req_stream);
2110
2111        // Ensure the state machine exits
2112        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2113    }
2114
2115    #[fuchsia::test]
2116    fn connected_state_gets_disconnect_request() {
2117        let mut exec = fasync::TestExecutor::new_with_fake_time();
2118        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2119
2120        let mut test_values = test_setup();
2121        let mut telemetry_receiver = test_values.telemetry_receiver;
2122        let connect_selection = generate_connect_selection();
2123        let bss_description =
2124            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2125        let init_ap_state =
2126            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2127
2128        let (connect_txn_proxy, _connect_txn_stream) =
2129            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2130        let options = ConnectedOptions::new(
2131            &mut test_values.common_options,
2132            Box::new(init_ap_state.clone()),
2133            connect_selection.target.network_has_multiple_bss,
2134            connect_selection.target.network.clone(),
2135            connect_selection.target.credential.clone(),
2136            connect_selection.reason,
2137            connect_txn_proxy.take_event_stream(),
2138            false,
2139        );
2140        let initial_state = connected_state(test_values.common_options, options);
2141        let fut = run_state_machine(initial_state);
2142        let mut fut = pin!(fut);
2143        let sme_fut = test_values.sme_req_stream.into_future();
2144        let mut sme_fut = pin!(sme_fut);
2145
2146        let disconnect_time =
2147            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2148
2149        // Run the state machine
2150        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2151
2152        // Verify roam monitor request was sent.
2153        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2154            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2155        });
2156
2157        // Run the state machine
2158        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2159
2160        // Run forward to get post connection signals metrics
2161        exec.set_fake_time(fasync::MonotonicInstant::after(
2162            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2163        ));
2164        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2165        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2166            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2167        });
2168
2169        // Run forward to get long duration signals metrics
2170        exec.set_fake_time(fasync::MonotonicInstant::after(
2171            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2172        ));
2173        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2174        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2175            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2176        });
2177
2178        // Run forward to disconnect time
2179        exec.set_fake_time(disconnect_time);
2180        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2181
2182        // Send a disconnect request
2183        let mut client = Client::new(test_values.client_req_sender);
2184        let (sender, mut receiver) = oneshot::channel();
2185        client
2186            .disconnect(types::DisconnectReason::FidlStopClientConnectionsRequest, sender)
2187            .expect("failed to make request");
2188
2189        // Run the state machine
2190        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2191
2192        // Respond to the SME disconnect
2193        assert_matches!(
2194            poll_sme_req(&mut exec, &mut sme_fut),
2195            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest }) => {
2196                responder.send().expect("could not send sme response");
2197            }
2198        );
2199
2200        // Once the disconnect is processed, the state machine should exit.
2201        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2202
2203        // Check for a disconnect update and the responder
2204        let client_state_update = ClientStateUpdate {
2205            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2206            networks: vec![ClientNetworkState {
2207                id: connect_selection.target.network.clone(),
2208                state: fidl_policy::ConnectionState::Disconnected,
2209                status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
2210            }],
2211        };
2212        assert_matches!(
2213            test_values.update_receiver.try_next(),
2214            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2215            assert_eq!(updates, client_state_update);
2216        });
2217        assert_matches!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(())));
2218
2219        // Disconnect telemetry event sent
2220        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2221            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2222                assert!(!track_subsequent_downtime);
2223                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2224                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2225                    assert!(!is_sme_reconnecting);
2226                    assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest));
2227                    assert_eq!(previous_connect_reason, connect_selection.reason);
2228                    assert_eq!(ap_state, init_ap_state.clone());
2229                });
2230            });
2231        });
2232
2233        // The disconnect should have been recorded for the saved network config.
2234        let expected_recorded_connection = ConnectionRecord {
2235            id: connect_selection.target.network.clone(),
2236            credential: connect_selection.target.credential.clone(),
2237            data: PastConnectionData {
2238                bssid: init_ap_state.original().bssid,
2239                disconnect_time,
2240                connection_uptime: zx::MonotonicDuration::from_hours(12),
2241                disconnect_reason: types::DisconnectReason::FidlStopClientConnectionsRequest,
2242                signal_at_disconnect: types::Signal {
2243                    rssi_dbm: bss_description.rssi_dbm,
2244                    snr_db: bss_description.snr_db,
2245                },
2246                // TODO: record average phy rate over connection once available
2247                average_tx_rate: 0,
2248            },
2249        };
2250        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2251            assert_eq!(connection_data, &expected_recorded_connection);
2252        });
2253    }
2254
2255    #[fuchsia::test]
2256    fn connected_state_records_unexpected_disconnect() {
2257        let mut exec = fasync::TestExecutor::new_with_fake_time();
2258        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2259
2260        let mut test_values = test_setup();
2261        let mut telemetry_receiver = test_values.telemetry_receiver;
2262
2263        let connect_selection = generate_connect_selection();
2264        let bss_description =
2265            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2266        let init_ap_state =
2267            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2268
2269        // Save the network in order to later record the disconnect to it.
2270        let save_fut = test_values.saved_networks_manager.store(
2271            connect_selection.target.network.clone(),
2272            connect_selection.target.credential.clone(),
2273        );
2274        let mut save_fut = pin!(save_fut);
2275        assert_matches!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
2276
2277        let (connect_txn_proxy, connect_txn_stream) =
2278            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2279        let connect_txn_handle = connect_txn_stream.control_handle();
2280        let options = ConnectedOptions::new(
2281            &mut test_values.common_options,
2282            Box::new(init_ap_state.clone()),
2283            connect_selection.target.network_has_multiple_bss,
2284            connect_selection.target.network.clone(),
2285            connect_selection.target.credential.clone(),
2286            connect_selection.reason,
2287            connect_txn_proxy.take_event_stream(),
2288            false,
2289        );
2290
2291        // Start the state machine in the connected state.
2292        let initial_state = connected_state(test_values.common_options, options);
2293        let fut = run_state_machine(initial_state);
2294        let mut fut = pin!(fut);
2295        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2296
2297        // Verify roam monitor request was sent.
2298        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2299            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2300        });
2301
2302        // Run the state machine
2303        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2304
2305        let disconnect_time =
2306            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2307        exec.set_fake_time(disconnect_time);
2308
2309        // SME notifies Policy of disconnection
2310        let fidl_disconnect_info = generate_disconnect_info(false);
2311        connect_txn_handle
2312            .send_on_disconnect(&fidl_disconnect_info)
2313            .expect("failed to send disconnection event");
2314        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2315
2316        // The disconnect should have been recorded for the saved network config.
2317        let expected_recorded_connection = ConnectionRecord {
2318            id: connect_selection.target.network.clone(),
2319            credential: connect_selection.target.credential.clone(),
2320            data: PastConnectionData {
2321                bssid: init_ap_state.original().bssid,
2322                disconnect_time,
2323                connection_uptime: zx::MonotonicDuration::from_hours(12),
2324                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
2325                signal_at_disconnect: types::Signal {
2326                    rssi_dbm: bss_description.rssi_dbm,
2327                    snr_db: bss_description.snr_db,
2328                },
2329                // TODO: record average phy rate over connection once available
2330                average_tx_rate: 0,
2331            },
2332        };
2333        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2334            assert_eq!(connection_data, &expected_recorded_connection);
2335        });
2336
2337        // Disconnect telemetry event sent
2338        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2339            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2340                assert!(track_subsequent_downtime);
2341                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2342                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2343                    assert!(!is_sme_reconnecting);
2344                    assert_eq!(disconnect_source, fidl_disconnect_info.disconnect_source);
2345                    assert_eq!(previous_connect_reason, connect_selection.reason);
2346                    assert_eq!(ap_state, init_ap_state);
2347                });
2348            });
2349        });
2350    }
2351
2352    #[fuchsia::test]
2353    fn connected_state_reconnect_resets_connected_duration() {
2354        let mut exec = fasync::TestExecutor::new_with_fake_time();
2355        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2356
2357        let mut test_values = test_setup();
2358        let mut telemetry_receiver = test_values.telemetry_receiver;
2359
2360        let connect_selection = generate_connect_selection();
2361        let bss_description =
2362            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2363        let ap_state =
2364            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2365
2366        let (connect_txn_proxy, connect_txn_stream) =
2367            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2368        let connect_txn_handle = connect_txn_stream.control_handle();
2369        let options = ConnectedOptions::new(
2370            &mut test_values.common_options,
2371            Box::new(ap_state.clone()),
2372            connect_selection.target.network_has_multiple_bss,
2373            connect_selection.target.network.clone(),
2374            connect_selection.target.credential.clone(),
2375            connect_selection.reason,
2376            connect_txn_proxy.take_event_stream(),
2377            false,
2378        );
2379        let initial_state = connected_state(test_values.common_options, options);
2380        let fut = run_state_machine(initial_state);
2381        let mut fut = pin!(fut);
2382
2383        let disconnect_time =
2384            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2385
2386        // Run the state machine
2387        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2388
2389        // Verify roam monitor request was sent.
2390        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2391            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2392        });
2393
2394        // Run the state machine
2395        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2396
2397        // Run forward to get post connection score metrics
2398        exec.set_fake_time(fasync::MonotonicInstant::after(
2399            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2400        ));
2401        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2402        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2403            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2404        });
2405
2406        // Run forward to get long duration signals metrics
2407        exec.set_fake_time(fasync::MonotonicInstant::after(
2408            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2409        ));
2410        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2411        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2412            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2413        });
2414
2415        // Run forward to disconnect time
2416        exec.set_fake_time(disconnect_time);
2417        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2418
2419        // SME notifies Policy of disconnection with SME-initiated reconnect
2420        let is_sme_reconnecting = true;
2421        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
2422        connect_txn_handle
2423            .send_on_disconnect(&fidl_disconnect_info)
2424            .expect("failed to send disconnection event");
2425        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2426
2427        // Disconnect telemetry event sent
2428        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2429            assert_matches!(event, TelemetryEvent::Disconnected { info: Some(info), .. } => {
2430                assert_eq!(info.connected_duration, zx::MonotonicDuration::from_hours(12));
2431            });
2432        });
2433
2434        // SME notifies Policy of reconnection successful
2435        exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1)));
2436        let connect_result =
2437            fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
2438        connect_txn_handle
2439            .send_on_connect_result(&connect_result)
2440            .expect("failed to send connect result event");
2441
2442        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2443        assert_matches!(
2444            telemetry_receiver.try_next(),
2445            Ok(Some(TelemetryEvent::ConnectResult { .. }))
2446        );
2447
2448        // SME notifies Policy of another disconnection
2449        exec.set_fake_time(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(2)));
2450        let is_sme_reconnecting = false;
2451        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
2452        connect_txn_handle
2453            .send_on_disconnect(&fidl_disconnect_info)
2454            .expect("failed to send disconnection event");
2455        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2456
2457        // Another disconnect telemetry event sent
2458        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2459            assert_matches!(event, TelemetryEvent::Disconnected { info, .. } => {
2460                assert_eq!(info.unwrap().connected_duration, zx::MonotonicDuration::from_hours(2));
2461            });
2462        });
2463    }
2464
2465    #[fuchsia::test]
2466    fn connected_state_records_unexpected_disconnect_unspecified_bss() {
2467        let mut exec = fasync::TestExecutor::new_with_fake_time();
2468        let connection_attempt_time = fasync::MonotonicInstant::from_nanos(0);
2469        exec.set_fake_time(connection_attempt_time);
2470        let mut test_values = test_setup();
2471
2472        let connect_selection = generate_connect_selection();
2473        let bss_description =
2474            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2475
2476        // Setup for network selection in the connecting state to select the intended network.
2477        let expected_config = network_config::NetworkConfig::new(
2478            connect_selection.target.network.clone(),
2479            connect_selection.target.credential.clone(),
2480            false,
2481            None,
2482        )
2483        .expect("failed to create network config");
2484        test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]);
2485
2486        let connecting_options =
2487            ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 };
2488        let initial_state = connecting_state(test_values.common_options, connecting_options);
2489        let state_fut = run_state_machine(initial_state);
2490        let mut state_fut = pin!(state_fut);
2491        let sme_fut = test_values.sme_req_stream.into_future();
2492        let mut sme_fut = pin!(sme_fut);
2493
2494        // Run the state machine
2495        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2496
2497        // Run the state machine
2498        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2499
2500        let time_to_connect = zx::MonotonicDuration::from_seconds(10);
2501        exec.set_fake_time(fasync::MonotonicInstant::after(time_to_connect));
2502
2503        // Process connect request sent to SME
2504        let connect_txn_handle = assert_matches!(
2505            poll_sme_req(&mut exec, &mut sme_fut),
2506            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => {
2507                 // Send connection response.
2508                let (_stream, ctrl) = txn.expect("connect txn unused")
2509                    .into_stream_and_control_handle();
2510                ctrl
2511            }
2512        );
2513        connect_txn_handle
2514            .send_on_connect_result(&fake_successful_connect_result())
2515            .expect("failed to send connection completion");
2516        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2517
2518        // SME notifies Policy of disconnection.
2519        let disconnect_time = fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(5));
2520        exec.set_fake_time(disconnect_time);
2521        let is_sme_reconnecting = false;
2522        connect_txn_handle
2523            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2524            .expect("failed to send disconnection event");
2525        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2526
2527        // Verify roam monitor request was sent.
2528        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2529            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2530        });
2531
2532        assert_matches!(exec.run_until_stalled(&mut state_fut), Poll::Pending);
2533        // The connection data should have been recorded at disconnect.
2534        let expected_recorded_connection = ConnectionRecord {
2535            id: connect_selection.target.network.clone(),
2536            credential: connect_selection.target.credential.clone(),
2537            data: PastConnectionData {
2538                bssid: types::Bssid::from(bss_description.bssid),
2539                disconnect_time,
2540                connection_uptime: zx::MonotonicDuration::from_hours(5),
2541                disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme,
2542                signal_at_disconnect: types::Signal {
2543                    rssi_dbm: bss_description.rssi_dbm,
2544                    snr_db: bss_description.snr_db,
2545                },
2546                average_tx_rate: 0,
2547            },
2548        };
2549        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2550            assert_eq!(connection_data, &expected_recorded_connection);
2551        });
2552    }
2553
2554    #[fuchsia::test]
2555    fn connected_state_gets_duplicate_connect_selection() {
2556        let mut exec = fasync::TestExecutor::new_with_fake_time();
2557        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2558        let mut test_values = test_setup();
2559        let mut telemetry_receiver = test_values.telemetry_receiver;
2560
2561        let connect_selection = generate_connect_selection();
2562        let bss_description =
2563            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2564        let ap_state =
2565            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2566
2567        let (connect_txn_proxy, _connect_txn_stream) =
2568            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2569        let options = ConnectedOptions::new(
2570            &mut test_values.common_options,
2571            Box::new(ap_state.clone()),
2572            connect_selection.target.network_has_multiple_bss,
2573            connect_selection.target.network.clone(),
2574            connect_selection.target.credential.clone(),
2575            connect_selection.reason,
2576            connect_txn_proxy.take_event_stream(),
2577            false,
2578        );
2579        let initial_state = connected_state(test_values.common_options, options);
2580        let fut = run_state_machine(initial_state);
2581        let mut fut = pin!(fut);
2582        let sme_fut = test_values.sme_req_stream.into_future();
2583        let mut sme_fut = pin!(sme_fut);
2584
2585        // Send another duplicate request
2586        let mut client = Client::new(test_values.client_req_sender);
2587        client.connect(connect_selection.clone()).expect("failed to make request");
2588
2589        // Run the state machine
2590        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2591
2592        // Ensure nothing was sent to the SME
2593        assert_matches!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending);
2594
2595        // No telemetry event is sent
2596        assert_matches!(telemetry_receiver.try_next(), Err(_));
2597    }
2598
2599    #[fuchsia::test]
2600    fn connected_state_gets_different_connect_selection() {
2601        let mut exec = fasync::TestExecutor::new_with_fake_time();
2602        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
2603
2604        let mut test_values = test_setup();
2605        let mut telemetry_receiver = test_values.telemetry_receiver;
2606
2607        let first_connect_selection = generate_connect_selection();
2608        let first_bss_desc =
2609            Sequestered::release(first_connect_selection.target.bss.bss_description.clone());
2610        let first_ap_state =
2611            types::ApState::from(BssDescription::try_from(first_bss_desc.clone()).unwrap());
2612        let second_connect_selection = types::ConnectSelection {
2613            reason: types::ConnectReason::ProactiveNetworkSwitch,
2614            ..generate_connect_selection()
2615        };
2616
2617        let (connect_txn_proxy, _connect_txn_stream) =
2618            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2619        let options = ConnectedOptions::new(
2620            &mut test_values.common_options,
2621            Box::new(first_ap_state.clone()),
2622            first_connect_selection.target.network_has_multiple_bss,
2623            first_connect_selection.target.network.clone(),
2624            first_connect_selection.target.credential.clone(),
2625            first_connect_selection.reason,
2626            connect_txn_proxy.take_event_stream(),
2627            false,
2628        );
2629        let initial_state = connected_state(test_values.common_options, options);
2630        let fut = run_state_machine(initial_state);
2631        let mut fut = pin!(fut);
2632        let sme_fut = test_values.sme_req_stream.into_future();
2633        let mut sme_fut = pin!(sme_fut);
2634
2635        let disconnect_time =
2636            fasync::MonotonicInstant::after(zx::MonotonicDuration::from_hours(12));
2637
2638        // Run the state machine
2639        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2640
2641        // Verify roam monitor request was sent.
2642        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2643            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2644        });
2645
2646        // Run the state machine
2647        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2648
2649        // Run forward to get post connection signals metrics
2650        exec.set_fake_time(fasync::MonotonicInstant::after(
2651            AVERAGE_SCORE_DELTA_MINIMUM_DURATION + zx::MonotonicDuration::from_seconds(1),
2652        ));
2653        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2654        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2655            assert_matches!(event, TelemetryEvent::PostConnectionSignals { .. });
2656        });
2657
2658        // Run forward to get long duration signals metrics
2659        exec.set_fake_time(fasync::MonotonicInstant::after(
2660            METRICS_SHORT_CONNECT_DURATION + zx::MonotonicDuration::from_seconds(1),
2661        ));
2662        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2663        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2664            assert_matches!(event, TelemetryEvent::LongDurationSignals { .. });
2665        });
2666
2667        // Run forward to disconnect time
2668        exec.set_fake_time(disconnect_time);
2669        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2670
2671        // Send a different connect request
2672        let mut client = Client::new(test_values.client_req_sender);
2673        client.connect(second_connect_selection.clone()).expect("failed to make request");
2674
2675        // Run the state machine
2676        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2677
2678        // There should be 2 requests to the SME stacked up
2679        // First SME request: disconnect
2680        assert_matches!(
2681            poll_sme_req(&mut exec, &mut sme_fut),
2682            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => {
2683                responder.send().expect("could not send sme response");
2684            }
2685        );
2686        // Progress the state machine
2687        // TODO(https://fxbug.dev/42130926): remove this once the disconnect request is fire-and-forget
2688        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2689        // Second SME request: connect to the second network
2690        let connect_txn_handle = assert_matches!(
2691            poll_sme_req(&mut exec, &mut sme_fut),
2692            Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => {
2693                assert_eq!(req.ssid, second_connect_selection.target.network.ssid.clone().to_vec());
2694                 // Send connection response.
2695                let (_stream, ctrl) = txn.expect("connect txn unused")
2696                    .into_stream_and_control_handle();
2697                ctrl
2698            }
2699        );
2700        connect_txn_handle
2701            .send_on_connect_result(&fake_successful_connect_result())
2702            .expect("failed to send connection completion");
2703        // Progress the state machine
2704        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2705
2706        // Check for a disconnect update
2707        let client_state_update = ClientStateUpdate {
2708            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2709            networks: vec![ClientNetworkState {
2710                id: first_connect_selection.target.network.clone(),
2711                state: fidl_policy::ConnectionState::Disconnected,
2712                status: Some(fidl_policy::DisconnectStatus::ConnectionStopped),
2713            }],
2714        };
2715        assert_matches!(
2716            test_values.update_receiver.try_next(),
2717            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2718            assert_eq!(updates, client_state_update);
2719        });
2720
2721        // Disconnect telemetry event sent
2722        assert_matches!(telemetry_receiver.try_next(), Ok(Some(event)) => {
2723            assert_matches!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info: Some(info) } => {
2724                assert!(!track_subsequent_downtime);
2725                assert_matches!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => {
2726                    assert_eq!(connected_duration, zx::MonotonicDuration::from_hours(12));
2727                    assert!(!is_sme_reconnecting);
2728                    assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch));
2729                    assert_eq!(previous_connect_reason, first_connect_selection.reason);
2730                    assert_eq!(ap_state, first_ap_state.clone());
2731                });
2732            });
2733        });
2734
2735        // Check for a connecting update
2736        let client_state_update = ClientStateUpdate {
2737            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2738            networks: vec![ClientNetworkState {
2739                id: types::NetworkIdentifier {
2740                    ssid: second_connect_selection.target.network.ssid.clone(),
2741                    security_type: types::SecurityType::Wpa2,
2742                },
2743                state: fidl_policy::ConnectionState::Connecting,
2744                status: None,
2745            }],
2746        };
2747        assert_matches!(
2748            test_values.update_receiver.try_next(),
2749            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2750            assert_eq!(updates, client_state_update);
2751        });
2752        // Check for a connected update
2753        let client_state_update = ClientStateUpdate {
2754            state: fidl_policy::WlanClientState::ConnectionsEnabled,
2755            networks: vec![ClientNetworkState {
2756                id: types::NetworkIdentifier {
2757                    ssid: second_connect_selection.target.network.ssid.clone(),
2758                    security_type: types::SecurityType::Wpa2,
2759                },
2760                state: fidl_policy::ConnectionState::Connected,
2761                status: None,
2762            }],
2763        };
2764        assert_matches!(
2765            test_values.update_receiver.try_next(),
2766            Ok(Some(listener::Message::NotifyListeners(updates))) => {
2767            assert_eq!(updates, client_state_update);
2768        });
2769
2770        // Progress the state machine
2771        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2772
2773        // Ensure no further updates were sent to listeners
2774        assert_matches!(
2775            exec.run_until_stalled(&mut test_values.update_receiver.into_future()),
2776            Poll::Pending
2777        );
2778
2779        // Check that the first connection was recorded
2780        let expected_recorded_connection = ConnectionRecord {
2781            id: first_connect_selection.target.network.clone(),
2782            credential: first_connect_selection.target.credential.clone(),
2783            data: PastConnectionData {
2784                bssid: types::Bssid::from(first_bss_desc.bssid),
2785                disconnect_time,
2786                connection_uptime: zx::MonotonicDuration::from_hours(12),
2787                disconnect_reason: types::DisconnectReason::ProactiveNetworkSwitch,
2788                signal_at_disconnect: types::Signal {
2789                    rssi_dbm: first_bss_desc.rssi_dbm,
2790                    snr_db: first_bss_desc.snr_db,
2791                },
2792                // TODO: record average phy rate over connection once available
2793                average_tx_rate: 0,
2794            },
2795        };
2796        assert_matches!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => {
2797            assert_eq!(connection_data, &expected_recorded_connection);
2798        });
2799    }
2800
2801    #[fuchsia::test]
2802    fn connected_state_notified_of_network_disconnect_no_sme_reconnect_short_uptime_no_retry() {
2803        let mut exec = fasync::TestExecutor::new_with_fake_time();
2804        let mut test_values = test_setup();
2805
2806        let connect_selection = generate_connect_selection();
2807        let bss_description =
2808            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2809        let ap_state =
2810            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2811
2812        let (connect_txn_proxy, connect_txn_stream) =
2813            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2814        let connect_txn_handle = connect_txn_stream.control_handle();
2815        let options = ConnectedOptions::new(
2816            &mut test_values.common_options,
2817            Box::new(ap_state.clone()),
2818            connect_selection.target.network_has_multiple_bss,
2819            connect_selection.target.network.clone(),
2820            connect_selection.target.credential.clone(),
2821            connect_selection.reason,
2822            connect_txn_proxy.take_event_stream(),
2823            false,
2824        );
2825        let initial_state = connected_state(test_values.common_options, options);
2826        let fut = run_state_machine(initial_state);
2827        let mut fut = pin!(fut);
2828        let sme_fut = test_values.sme_req_stream.into_future();
2829        let mut sme_fut = pin!(sme_fut);
2830
2831        // Run the state machine
2832        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2833
2834        // Verify roam monitor request was sent.
2835        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2836            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2837        });
2838
2839        // Run the state machine
2840        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2841
2842        // SME notifies Policy of disconnection.
2843        let is_sme_reconnecting = false;
2844        connect_txn_handle
2845            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2846            .expect("failed to send disconnection event");
2847
2848        // Run the state machine
2849        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2850
2851        // Check for a disconnect request to SME
2852        assert_matches!(
2853            poll_sme_req(&mut exec, &mut sme_fut),
2854            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
2855                responder.send().expect("could not send sme response");
2856            }
2857        );
2858
2859        // The state machine should exit since there is no attempt to reconnect.
2860        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2861    }
2862
2863    #[fuchsia::test]
2864    fn connected_state_notified_of_network_disconnect_sme_reconnect_successfully() {
2865        let mut exec = fasync::TestExecutor::new();
2866        let mut test_values = test_setup();
2867
2868        let connect_selection = generate_connect_selection();
2869        let bss_description =
2870            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2871        let ap_state =
2872            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2873
2874        let (connect_txn_proxy, connect_txn_stream) =
2875            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2876        let connect_txn_handle = connect_txn_stream.control_handle();
2877        let options = ConnectedOptions::new(
2878            &mut test_values.common_options,
2879            Box::new(ap_state.clone()),
2880            connect_selection.target.network_has_multiple_bss,
2881            connect_selection.target.network.clone(),
2882            connect_selection.target.credential.clone(),
2883            connect_selection.reason,
2884            connect_txn_proxy.take_event_stream(),
2885            false,
2886        );
2887        let initial_state = connected_state(test_values.common_options, options);
2888        let fut = run_state_machine(initial_state);
2889        let mut fut = pin!(fut);
2890
2891        // Run the state machine
2892        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2893
2894        // SME notifies Policy of disconnection
2895        let is_sme_reconnecting = true;
2896        connect_txn_handle
2897            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2898            .expect("failed to send disconnection event");
2899
2900        // Run the state machine
2901        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2902
2903        // SME notifies Policy that reconnects succeeds
2904        let connect_result =
2905            fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() };
2906        connect_txn_handle
2907            .send_on_connect_result(&connect_result)
2908            .expect("failed to send reconnection result");
2909
2910        // Run the state machine
2911        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2912
2913        // Check there were no state updates
2914        assert_matches!(test_values.update_receiver.try_next(), Err(_));
2915    }
2916
2917    #[fuchsia::test]
2918    fn connected_state_notified_of_network_disconnect_sme_reconnect_unsuccessfully() {
2919        let mut exec = fasync::TestExecutor::new_with_fake_time();
2920        let mut test_values = test_setup();
2921        let connect_selection = generate_connect_selection();
2922        let bss_description =
2923            Sequestered::release(connect_selection.target.bss.bss_description.clone());
2924        let ap_state =
2925            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
2926
2927        // Set the start time of the connection
2928        let start_time = fasync::MonotonicInstant::now();
2929        exec.set_fake_time(start_time);
2930
2931        let (connect_txn_proxy, connect_txn_stream) =
2932            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
2933        let connect_txn_handle = connect_txn_stream.control_handle();
2934        let options = ConnectedOptions::new(
2935            &mut test_values.common_options,
2936            Box::new(ap_state.clone()),
2937            connect_selection.target.network_has_multiple_bss,
2938            connect_selection.target.network.clone(),
2939            connect_selection.target.credential.clone(),
2940            connect_selection.reason,
2941            connect_txn_proxy.take_event_stream(),
2942            false,
2943        );
2944        let initial_state = connected_state(test_values.common_options, options);
2945        let fut = run_state_machine(initial_state);
2946        let mut fut = pin!(fut);
2947        let sme_fut = test_values.sme_req_stream.into_future();
2948        let mut sme_fut = pin!(sme_fut);
2949
2950        // Run the state machine
2951        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2952
2953        // Verify roam monitor request was sent.
2954        assert_matches!(test_values.roam_service_request_receiver.try_next(), Ok(Some(request)) => {
2955            assert_matches!(request, RoamServiceRequest::InitializeRoamMonitor { .. });
2956        });
2957
2958        // Run the state machine
2959        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2960
2961        // Set time to indicate a decent uptime before the disconnect so the AP is retried
2962        exec.set_fake_time(start_time + fasync::MonotonicDuration::from_hours(24));
2963
2964        // SME notifies Policy of disconnection
2965        let is_sme_reconnecting = true;
2966        connect_txn_handle
2967            .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting))
2968            .expect("failed to send disconnection event");
2969
2970        // Run the state machine
2971        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2972
2973        // SME notifies Policy that reconnects fails
2974        let connect_result = fidl_sme::ConnectResult {
2975            code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
2976            is_reconnect: true,
2977            ..fake_successful_connect_result()
2978        };
2979        connect_txn_handle
2980            .send_on_connect_result(&connect_result)
2981            .expect("failed to send reconnection result");
2982
2983        // Run the state machine
2984        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
2985
2986        // Check for an SME disconnect request
2987        assert_matches!(
2988            poll_sme_req(&mut exec, &mut sme_fut),
2989            Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => {
2990                responder.send().expect("could not send sme response");
2991            }
2992        );
2993
2994        // The state machine should exit since there is no policy attempt to reconnect.
2995        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
2996
2997        // Check for a disconnect update
2998        let client_state_update = ClientStateUpdate {
2999            state: fidl_policy::WlanClientState::ConnectionsEnabled,
3000            networks: vec![ClientNetworkState {
3001                id: connect_selection.target.network.clone(),
3002                state: fidl_policy::ConnectionState::Disconnected,
3003                status: Some(fidl_policy::DisconnectStatus::ConnectionFailed),
3004            }],
3005        };
3006        assert_matches!(
3007            test_values.update_receiver.try_next(),
3008            Ok(Some(listener::Message::NotifyListeners(updates))) => {
3009            assert_eq!(updates, client_state_update);
3010        });
3011    }
3012
3013    #[fuchsia::test]
3014    fn connected_state_on_signal_report() {
3015        let mut exec = fasync::TestExecutor::new_with_fake_time();
3016        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
3017
3018        let mut test_values = test_setup();
3019
3020        // Verify the status is initialized to default.
3021        let status = test_values.status_reader.read_status().expect("failed to read status");
3022        assert_matches!(status, Status::Disconnected);
3023
3024        // Set initial RSSI and SNR values
3025        let mut connect_selection = generate_connect_selection();
3026        let init_rssi = -40;
3027        let init_snr = 30;
3028        connect_selection.target.bss.signal =
3029            types::Signal { rssi_dbm: init_rssi, snr_db: init_snr };
3030
3031        let mut bss_description =
3032            Sequestered::release(connect_selection.target.bss.bss_description.clone());
3033        bss_description.rssi_dbm = init_rssi;
3034        bss_description.snr_db = init_snr;
3035        connect_selection.target.bss.bss_description = bss_description.clone().into();
3036
3037        let ap_state =
3038            types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap());
3039
3040        // Add a PastConnectionData for the connected network to be send in BSS quality data.
3041        let mut past_connections = PastConnectionList::default();
3042        let mut past_connection_data = random_connection_data();
3043        past_connection_data.bssid = ieee80211::Bssid::from(bss_description.bssid);
3044        past_connections.add(past_connection_data);
3045        let mut saved_networks_manager = FakeSavedNetworksManager::new();
3046        saved_networks_manager.past_connections_response = past_connections.clone();
3047        test_values.common_options.saved_networks_manager = Arc::new(saved_networks_manager);
3048
3049        // Set up the state machine, starting at the connected state.
3050        let (connect_txn_proxy, connect_txn_stream) =
3051            create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>();
3052        let options = ConnectedOptions::new(
3053            &mut test_values.common_options,
3054            Box::new(ap_state.clone()),
3055            connect_selection.target.network_has_multiple_bss,
3056            connect_selection.target.network.clone(),
3057            connect_selection.target.credential.clone(),
3058            connect_selection.reason,
3059            connect_txn_proxy.take_event_stream(),
3060            false,
3061        );
3062        let initial_state = connected_state(test_values.common_options, options);
3063
3064        let connect_txn_handle = connect_txn_stream.control_handle();
3065        let fut = run_state_machine(initial_state);
3066        let mut fut = pin!(fut);
3067        let sme_fut = test_values.sme_req_stream.into_future();
3068        let mut sme_fut = pin!(sme_fut);
3069
3070        // Run the state machine
3071        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
3072
3073        let request = test_values
3074            .roam_service_request_receiver
3075            .try_next()
3076            .expect("error receiving roam service request")
3077            .expect("received None 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 };
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_next(), Ok(Some(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_next(), Ok(Some(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 };
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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), 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_next(), Ok(Some(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_next(),
3482            Ok(Some(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 })))
3483        );
3484
3485        // Verify telemetry event for roam result
3486        assert_matches!(telemetry_receiver.try_next(), Ok(Some(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_next(), Err(_));
3495
3496        // Verify the roam monitor was _not_ re-initialized.
3497        assert_matches!(test_values.roam_service_request_receiver.try_next(), 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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(),
3616            Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(),
3725            Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(),
3819            Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(), Ok(Some(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_next(),
3938            Ok(Some(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_next(),
3985            Ok(Some(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_next(),
4051            Ok(Some(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(Some(event)) = test_values.telemetry_receiver.try_next() {
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_next(), Ok(Some(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}