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