fxa_client/state_machine/
checker.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! This module contains code to dry-run test the state machine in against the existing Android/iOS implementations.
6//! The idea is to create a `FxaStateChecker` instance, manually drive the state transitions and
7//! check its state against the FirefoxAccount calls the existing implementation makes.
8//!
9//! Initially this will be tested by devs / QA.  Then we will ship it to real users and monitor which errors come back in Sentry.
10
11use crate::{FxaEvent, FxaState};
12use error_support::{breadcrumb, report_error};
13use parking_lot::Mutex;
14
15pub use super::internal_machines::Event as FxaStateCheckerEvent;
16use super::internal_machines::State as InternalState;
17use super::internal_machines::*;
18
19/// State passed to the state checker, this is exactly the same as `internal_machines::State`
20/// except the `Complete` variant uses a named field for UniFFI compatibility.
21pub enum FxaStateCheckerState {
22    GetAuthState,
23    BeginOAuthFlow {
24        scopes: Vec<String>,
25        entrypoint: String,
26    },
27    BeginPairingFlow {
28        pairing_url: String,
29        scopes: Vec<String>,
30        entrypoint: String,
31    },
32    CompleteOAuthFlow {
33        code: String,
34        state: String,
35    },
36    InitializeDevice,
37    EnsureDeviceCapabilities,
38    CheckAuthorizationStatus,
39    GetProfile,
40    Disconnect,
41    Complete {
42        new_state: FxaState,
43    },
44    Cancel,
45}
46
47pub struct FxaStateMachineChecker {
48    inner: Mutex<FxaStateMachineCheckerInner>,
49}
50
51struct FxaStateMachineCheckerInner {
52    public_state: FxaState,
53    internal_state: InternalState,
54    state_machine: Box<dyn InternalStateMachine + Send>,
55    // Did we report an error?  If so, then we should give up checking things since the error is
56    // likely to cascade
57    reported_error: bool,
58}
59
60impl Default for FxaStateMachineChecker {
61    fn default() -> Self {
62        Self {
63            inner: Mutex::new(FxaStateMachineCheckerInner {
64                public_state: FxaState::Uninitialized,
65                internal_state: InternalState::Cancel,
66                state_machine: Box::new(UninitializedStateMachine),
67                reported_error: false,
68            }),
69        }
70    }
71}
72
73impl FxaStateMachineChecker {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Advance the internal state based on a public event
79    pub fn handle_public_event(&self, event: FxaEvent) {
80        let mut inner = self.inner.lock();
81        if inner.reported_error {
82            return;
83        }
84        match &inner.internal_state {
85            InternalState::Complete(_) | InternalState::Cancel => (),
86            internal_state => {
87                report_error!(
88                    "fxa-state-machine-checker",
89                    "handle_public_event called with non-terminal internal state (event: {event}, internal state: {internal_state})",
90                 );
91                inner.reported_error = true;
92                return;
93            }
94        }
95
96        inner.state_machine = make_state_machine(&inner.public_state);
97        match inner.state_machine.initial_state(event.clone()) {
98            Ok(state) => {
99                breadcrumb!(
100                    "fxa-state-machine-checker: public transition start ({event} -> {state})"
101                );
102                inner.internal_state = state;
103                if let InternalState::Complete(new_state) = &inner.internal_state {
104                    inner.public_state = new_state.clone();
105                    breadcrumb!("fxa-state-machine-checker: public transition end");
106                }
107            }
108            Err(e) => {
109                report_error!(
110                    "fxa-state-machine-checker",
111                    "Error in handle_public_event: {e}"
112                );
113                inner.reported_error = true;
114            }
115        }
116    }
117
118    /// Advance the internal state based on an internal event
119    pub fn handle_internal_event(&self, event: FxaStateCheckerEvent) {
120        let mut inner = self.inner.lock();
121        if inner.reported_error {
122            return;
123        }
124        match inner
125            .state_machine
126            .next_state(inner.internal_state.clone(), event.clone())
127        {
128            Ok(state) => {
129                breadcrumb!("fxa-state-machine-checker: internal transition ({event} -> {state})");
130                match &state {
131                    InternalState::Complete(new_state) => {
132                        inner.public_state = new_state.clone();
133                        breadcrumb!("fxa-state-machine-checker: public transition end");
134                    }
135                    InternalState::Cancel => {
136                        breadcrumb!("fxa-state-machine-checker: public transition end (cancelled)");
137                    }
138                    _ => (),
139                };
140                inner.internal_state = state;
141            }
142            Err(e) => {
143                report_error!(
144                    "fxa-state-machine-checker",
145                    "Error in handle_internal_event: {e}"
146                );
147                inner.reported_error = true;
148            }
149        }
150    }
151
152    /// Check the internal state
153    ///
154    /// Call this when `processQueue`/`processEvent` has advanced the existing state machine to a public state.
155    pub fn check_public_state(&self, state: FxaState) {
156        let mut inner = self.inner.lock();
157        if inner.reported_error {
158            return;
159        }
160        match &inner.internal_state {
161            InternalState::Complete(_) | InternalState::Cancel => (),
162            internal_state => {
163                report_error!(
164                    "fxa-state-machine-checker",
165                    "check_public_state called with non-terminal internal state (expected: {state} actual internal state: {internal_state})"
166                 );
167                inner.reported_error = true;
168                return;
169            }
170        }
171        if inner.public_state != state {
172            report_error!(
173                "fxa-state-machine-checker",
174                "Public state mismatch: expected: {state}, actual: {} ({})",
175                inner.public_state,
176                inner.internal_state
177            );
178            inner.reported_error = true;
179        } else {
180            breadcrumb!("fxa-state-machine-checker: check_public_state successful {state}");
181        }
182    }
183
184    /// Check the internal state
185    ///
186    /// Call this when a FirefoxAccount call is about to be made
187    pub fn check_internal_state(&self, state: FxaStateCheckerState) {
188        let mut inner = self.inner.lock();
189        if inner.reported_error {
190            return;
191        }
192        let state: InternalState = state.into();
193        if inner.internal_state != state {
194            report_error!(
195                "fxa-state-machine-checker",
196                "Internal state mismatch (expected: {state}, actual: {})",
197                inner.internal_state
198            );
199            inner.reported_error = true;
200        } else {
201            breadcrumb!("fxa-state-machine-checker: check_internal_state successful {state}");
202        }
203    }
204}
205
206fn make_state_machine(public_state: &FxaState) -> Box<dyn InternalStateMachine + Send> {
207    match public_state {
208        FxaState::Uninitialized => Box::new(UninitializedStateMachine),
209        FxaState::Disconnected => Box::new(DisconnectedStateMachine),
210        FxaState::Authenticating { .. } => Box::new(AuthenticatingStateMachine),
211        FxaState::Connected => Box::new(ConnectedStateMachine),
212        FxaState::AuthIssues => Box::new(AuthIssuesStateMachine),
213    }
214}
215
216impl From<InternalState> for FxaStateCheckerState {
217    fn from(state: InternalState) -> Self {
218        match state {
219            InternalState::GetAuthState => Self::GetAuthState,
220            InternalState::BeginOAuthFlow { scopes, entrypoint } => {
221                Self::BeginOAuthFlow { scopes, entrypoint }
222            }
223            InternalState::BeginPairingFlow {
224                pairing_url,
225                scopes,
226                entrypoint,
227            } => Self::BeginPairingFlow {
228                pairing_url,
229                scopes,
230                entrypoint,
231            },
232            InternalState::CompleteOAuthFlow { code, state } => {
233                Self::CompleteOAuthFlow { code, state }
234            }
235            InternalState::InitializeDevice => Self::InitializeDevice,
236            InternalState::EnsureDeviceCapabilities => Self::EnsureDeviceCapabilities,
237            InternalState::CheckAuthorizationStatus => Self::CheckAuthorizationStatus,
238            InternalState::Disconnect => Self::Disconnect,
239            InternalState::GetProfile => Self::GetProfile,
240            InternalState::Complete(new_state) => Self::Complete { new_state },
241            InternalState::Cancel => Self::Cancel,
242        }
243    }
244}
245
246impl From<FxaStateCheckerState> for InternalState {
247    fn from(state: FxaStateCheckerState) -> Self {
248        match state {
249            FxaStateCheckerState::GetAuthState => Self::GetAuthState,
250            FxaStateCheckerState::BeginOAuthFlow { scopes, entrypoint } => {
251                Self::BeginOAuthFlow { scopes, entrypoint }
252            }
253            FxaStateCheckerState::BeginPairingFlow {
254                pairing_url,
255                scopes,
256                entrypoint,
257            } => Self::BeginPairingFlow {
258                pairing_url,
259                scopes,
260                entrypoint,
261            },
262            FxaStateCheckerState::CompleteOAuthFlow { code, state } => {
263                Self::CompleteOAuthFlow { code, state }
264            }
265            FxaStateCheckerState::InitializeDevice => Self::InitializeDevice,
266            FxaStateCheckerState::EnsureDeviceCapabilities => Self::EnsureDeviceCapabilities,
267            FxaStateCheckerState::CheckAuthorizationStatus => Self::CheckAuthorizationStatus,
268            FxaStateCheckerState::Disconnect => Self::Disconnect,
269            FxaStateCheckerState::GetProfile => Self::GetProfile,
270            FxaStateCheckerState::Complete { new_state } => Self::Complete(new_state),
271            FxaStateCheckerState::Cancel => Self::Cancel,
272        }
273    }
274}