sync15/client/
state.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
5use std::collections::{HashMap, HashSet};
6
7use super::request::{InfoCollections, InfoConfiguration};
8use super::storage_client::{SetupStorageClient, Sync15ClientResponse};
9use super::CollectionKeys;
10use crate::bso::OutgoingEncryptedBso;
11use crate::error::{self, debug, info, trace, warn, Error as ErrorKind, ErrorResponse};
12use crate::record_types::{MetaGlobalEngine, MetaGlobalRecord};
13use crate::EncryptedPayload;
14use crate::{Guid, KeyBundle, ServerTimestamp};
15use interrupt_support::Interruptee;
16use serde_derive::*;
17
18use self::SetupState::*;
19
20const STORAGE_VERSION: usize = 5;
21
22/// Maps names to storage versions for engines to include in a fresh
23/// `meta/global` record. We include engines that we don't implement
24/// because they'll be disabled on other clients if we omit them
25/// (bug 1479929).
26const DEFAULT_ENGINES: &[(&str, usize)] = &[
27    ("passwords", 1),
28    ("clients", 1),
29    ("addons", 1),
30    ("addresses", 1),
31    ("bookmarks", 2),
32    ("creditcards", 1),
33    ("forms", 1),
34    ("history", 1),
35    ("prefs", 2),
36    ("tabs", 1),
37];
38
39// Declined engines to include in a fresh `meta/global` record.
40const DEFAULT_DECLINED: &[&str] = &[];
41
42/// State that we require the app to persist to storage for us.
43/// It's a little unfortunate we need this, because it's only tracking
44/// "declined engines", and even then, only needed in practice when there's
45/// no meta/global so we need to create one. It's extra unfortunate because we
46/// want to move away from "globally declined" engines anyway, moving towards
47/// allowing engines to be enabled or disabled per client rather than globally.
48///
49/// Apps are expected to treat this as opaque, so we support serializing it.
50/// Note that this structure is *not* used to *change* the declined engines
51/// list - that will be done in the future, but the API exposed for that
52/// purpose will also take a mutable PersistedGlobalState.
53#[derive(Debug, Serialize, Deserialize)]
54#[serde(tag = "schema_version")]
55pub enum PersistedGlobalState {
56    // V1 was when we persisted the entire GlobalState, keys and all!
57    /// V2 is just tracking the globally declined list.
58    /// None means "I've no idea" and theoretically should only happen on the
59    /// very first sync for an app.
60    V2 { declined: Option<Vec<String>> },
61}
62
63impl Default for PersistedGlobalState {
64    #[inline]
65    fn default() -> PersistedGlobalState {
66        PersistedGlobalState::V2 { declined: None }
67    }
68}
69
70#[derive(Debug, Default, Clone, PartialEq)]
71pub(crate) struct EngineChangesNeeded {
72    pub local_resets: HashSet<String>,
73    pub remote_wipes: HashSet<String>,
74}
75
76#[derive(Debug, Default, Clone, PartialEq)]
77struct RemoteEngineState {
78    info_collections: HashSet<String>,
79    declined: HashSet<String>,
80}
81
82#[derive(Debug, Default, Clone, PartialEq)]
83struct EngineStateInput {
84    local_declined: HashSet<String>,
85    remote: Option<RemoteEngineState>,
86    user_changes: HashMap<String, bool>,
87}
88
89#[derive(Debug, Default, Clone, PartialEq)]
90struct EngineStateOutput {
91    // The new declined.
92    declined: HashSet<String>,
93    // Which engines need resets or wipes.
94    changes_needed: EngineChangesNeeded,
95}
96
97fn compute_engine_states(input: EngineStateInput) -> EngineStateOutput {
98    use super::util::*;
99    debug!("compute_engine_states: input {:?}", input);
100    let (must_enable, must_disable) = partition_by_value(&input.user_changes);
101    let have_remote = input.remote.is_some();
102    let RemoteEngineState {
103        info_collections,
104        declined: remote_declined,
105    } = input.remote.clone().unwrap_or_default();
106
107    let both_declined_and_remote = set_intersection(&info_collections, &remote_declined);
108    if !both_declined_and_remote.is_empty() {
109        // Should we wipe these too?
110        warn!(
111            "Remote state contains engines which are in both info/collections and meta/global's declined: {:?}",
112            both_declined_and_remote,
113        );
114    }
115
116    let most_recent_declined_list = if have_remote {
117        &remote_declined
118    } else {
119        &input.local_declined
120    };
121
122    let result_declined = set_difference(
123        &set_union(most_recent_declined_list, &must_disable),
124        &must_enable,
125    );
126
127    let output = EngineStateOutput {
128        changes_needed: EngineChangesNeeded {
129            // Anything now declined which wasn't in our declined list before gets a reset.
130            local_resets: set_difference(&result_declined, &input.local_declined),
131            // Anything remote that we just declined gets a wipe. In the future
132            // we might want to consider wiping things in both remote declined
133            // and info/collections, but we'll let other clients pick up their
134            // own mess for now.
135            remote_wipes: set_intersection(&info_collections, &must_disable),
136        },
137        declined: result_declined,
138    };
139    // No PII here and this helps debug problems.
140    debug!("compute_engine_states: output {:?}", output);
141    output
142}
143
144impl PersistedGlobalState {
145    fn set_declined(&mut self, new_declined: Vec<String>) {
146        match self {
147            Self::V2 { ref mut declined } => *declined = Some(new_declined),
148        }
149    }
150    pub(crate) fn get_declined(&self) -> &[String] {
151        match self {
152            Self::V2 { declined: Some(d) } => d,
153            Self::V2 { declined: None } => &[],
154        }
155    }
156}
157
158/// Holds global Sync state, including server upload limits, the
159/// last-fetched collection modified times, `meta/global` record, and
160/// an encrypted copy of the crypto/keys resource (avoids keeping them
161/// in memory longer than necessary; avoids key mismatches by ensuring the same KeyBundle
162/// is used for both the keys and encrypted payloads.)
163#[derive(Debug, Clone)]
164pub struct GlobalState {
165    pub config: InfoConfiguration,
166    pub collections: InfoCollections,
167    pub global: MetaGlobalRecord,
168    pub global_timestamp: ServerTimestamp,
169    pub keys: EncryptedPayload,
170    pub keys_timestamp: ServerTimestamp,
171}
172
173/// Creates a fresh `meta/global` record, using the default engine selections,
174/// and declined engines from our PersistedGlobalState.
175fn new_global(pgs: &PersistedGlobalState) -> MetaGlobalRecord {
176    let sync_id = Guid::random();
177    let mut engines: HashMap<String, _> = HashMap::new();
178    for (name, version) in DEFAULT_ENGINES.iter() {
179        let sync_id = Guid::random();
180        engines.insert(
181            (*name).to_string(),
182            MetaGlobalEngine {
183                version: *version,
184                sync_id,
185            },
186        );
187    }
188    // We only need our PersistedGlobalState to fill out a new meta/global - if
189    // we previously saw a meta/global then we would have updated it with what
190    // it was at the time.
191    let declined = match pgs {
192        PersistedGlobalState::V2 { declined: Some(d) } => d.clone(),
193        _ => DEFAULT_DECLINED.iter().map(ToString::to_string).collect(),
194    };
195
196    MetaGlobalRecord {
197        sync_id,
198        storage_version: STORAGE_VERSION,
199        engines,
200        declined,
201    }
202}
203
204fn fixup_meta_global(global: &mut MetaGlobalRecord) -> bool {
205    let mut changed_any = false;
206    for &(name, version) in DEFAULT_ENGINES.iter() {
207        let had_engine = global.engines.contains_key(name);
208        let should_have_engine = !global.declined.iter().any(|c| c == name);
209        if had_engine != should_have_engine {
210            if should_have_engine {
211                debug!("SyncID for engine {:?} was missing", name);
212                global.engines.insert(
213                    name.to_string(),
214                    MetaGlobalEngine {
215                        version,
216                        sync_id: Guid::random(),
217                    },
218                );
219            } else {
220                debug!("SyncID for engine {:?} was present, but shouldn't be", name);
221                global.engines.remove(name);
222            }
223            changed_any = true;
224        }
225    }
226    changed_any
227}
228
229pub struct SetupStateMachine<'a> {
230    client: &'a dyn SetupStorageClient,
231    root_key: &'a KeyBundle,
232    pgs: &'a mut PersistedGlobalState,
233    // `allowed_states` is designed so that we can arrange for the concept of
234    // a "fast" sync - so we decline to advance if we need to setup from scratch.
235    // The idea is that if we need to sync before going to sleep we should do
236    // it as fast as possible. However, in practice this isn't going to do
237    // what we expect - a "fast sync" that finds lots to do is almost certainly
238    // going to take longer than a "full sync" that finds nothing to do.
239    // We should almost certainly remove this and instead allow for a "time
240    // budget", after which we get interrupted. Later...
241    allowed_states: Vec<&'static str>,
242    sequence: Vec<&'static str>,
243    engine_updates: Option<&'a HashMap<String, bool>>,
244    interruptee: &'a dyn Interruptee,
245    pub(crate) changes_needed: Option<EngineChangesNeeded>,
246}
247
248impl<'a> SetupStateMachine<'a> {
249    /// Creates a state machine for a "classic" Sync 1.5 client that supports
250    /// all states, including uploading a fresh `meta/global` and `crypto/keys`
251    /// after a node reassignment.
252    pub fn for_full_sync(
253        client: &'a dyn SetupStorageClient,
254        root_key: &'a KeyBundle,
255        pgs: &'a mut PersistedGlobalState,
256        engine_updates: Option<&'a HashMap<String, bool>>,
257        interruptee: &'a dyn Interruptee,
258    ) -> SetupStateMachine<'a> {
259        SetupStateMachine::with_allowed_states(
260            client,
261            root_key,
262            pgs,
263            interruptee,
264            engine_updates,
265            vec![
266                "Initial",
267                "InitialWithConfig",
268                "InitialWithInfo",
269                "InitialWithMetaGlobal",
270                "Ready",
271                "FreshStartRequired",
272                "WithPreviousState",
273            ],
274        )
275    }
276
277    fn with_allowed_states(
278        client: &'a dyn SetupStorageClient,
279        root_key: &'a KeyBundle,
280        pgs: &'a mut PersistedGlobalState,
281        interruptee: &'a dyn Interruptee,
282        engine_updates: Option<&'a HashMap<String, bool>>,
283        allowed_states: Vec<&'static str>,
284    ) -> SetupStateMachine<'a> {
285        SetupStateMachine {
286            client,
287            root_key,
288            pgs,
289            sequence: Vec::new(),
290            allowed_states,
291            engine_updates,
292            interruptee,
293            changes_needed: None,
294        }
295    }
296
297    fn advance(&mut self, from: SetupState) -> error::Result<SetupState> {
298        match from {
299            // Fetch `info/configuration` with current server limits, and
300            // `info/collections` with collection last modified times.
301            Initial => {
302                let config = match self.client.fetch_info_configuration()? {
303                    Sync15ClientResponse::Success { record, .. } => record,
304                    Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
305                        InfoConfiguration::default()
306                    }
307                    other => return Err(other.create_storage_error()),
308                };
309                Ok(InitialWithConfig { config })
310            }
311
312            // XXX - we could consider combining these Initial* states, because we don't
313            // attempt to support filling in "missing" global state - *any* 404 in them
314            // means `FreshStart`.
315            // IOW, in all cases, they either `Err()`, move to `FreshStartRequired`, or
316            // advance to a specific next state.
317            InitialWithConfig { config } => {
318                match self.client.fetch_info_collections()? {
319                    Sync15ClientResponse::Success {
320                        record: collections,
321                        ..
322                    } => Ok(InitialWithInfo {
323                        config,
324                        collections,
325                    }),
326                    // If the server doesn't have a `crypto/keys`, start over
327                    // and reupload our `meta/global` and `crypto/keys`.
328                    Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
329                        Ok(FreshStartRequired { config })
330                    }
331                    other => Err(other.create_storage_error()),
332                }
333            }
334
335            InitialWithInfo {
336                config,
337                collections,
338            } => {
339                match self.client.fetch_meta_global()? {
340                    Sync15ClientResponse::Success {
341                        record: mut global,
342                        last_modified: mut global_timestamp,
343                        ..
344                    } => {
345                        // If the server has a newer storage version, we can't
346                        // sync until our client is updated.
347                        if global.storage_version > STORAGE_VERSION {
348                            return Err(ErrorKind::ClientUpgradeRequired);
349                        }
350
351                        // If the server has an older storage version, wipe and
352                        // reupload.
353                        if global.storage_version < STORAGE_VERSION {
354                            Ok(FreshStartRequired { config })
355                        } else {
356                            info!("Have info/collections and meta/global. Computing new engine states");
357                            let initial_global_declined: HashSet<String> =
358                                global.declined.iter().cloned().collect();
359                            let result = compute_engine_states(EngineStateInput {
360                                local_declined: self.pgs.get_declined().iter().cloned().collect(),
361                                user_changes: self.engine_updates.cloned().unwrap_or_default(),
362                                remote: Some(RemoteEngineState {
363                                    declined: initial_global_declined.clone(),
364                                    info_collections: collections.keys().cloned().collect(),
365                                }),
366                            });
367                            // Persist the new declined.
368                            self.pgs
369                                .set_declined(result.declined.iter().cloned().collect());
370                            // If the declined engines differ from remote, fix that.
371                            let fixed_declined = if result.declined != initial_global_declined {
372                                global.declined = result.declined.iter().cloned().collect();
373                                info!(
374                                    "Uploading new declined {:?} to meta/global with timestamp {:?}",
375                                    global.declined,
376                                    global_timestamp,
377                                );
378                                true
379                            } else {
380                                false
381                            };
382                            // If there are missing syncIds, we need to fix those as well
383                            let fixed_ids = if fixup_meta_global(&mut global) {
384                                info!(
385                                    "Uploading corrected meta/global with timestamp {:?}",
386                                    global_timestamp,
387                                );
388                                true
389                            } else {
390                                false
391                            };
392
393                            if fixed_declined || fixed_ids {
394                                global_timestamp =
395                                    self.client.put_meta_global(global_timestamp, &global)?;
396                                debug!("new global_timestamp: {:?}", global_timestamp);
397                            }
398                            // Update the set of changes needed.
399                            if self.changes_needed.is_some() {
400                                // Should never happen (we prevent state machine
401                                // loops elsewhere) but if it did, the info is stale
402                                // anyway.
403                                warn!("Already have a set of changes needed, Overwriting...");
404                            }
405                            self.changes_needed = Some(result.changes_needed);
406                            Ok(InitialWithMetaGlobal {
407                                config,
408                                collections,
409                                global,
410                                global_timestamp,
411                            })
412                        }
413                    }
414                    Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
415                        Ok(FreshStartRequired { config })
416                    }
417                    other => Err(other.create_storage_error()),
418                }
419            }
420
421            InitialWithMetaGlobal {
422                config,
423                collections,
424                global,
425                global_timestamp,
426            } => {
427                // Now try and get keys etc - if we fresh-start we'll re-use declined.
428                match self.client.fetch_crypto_keys()? {
429                    Sync15ClientResponse::Success {
430                        record,
431                        last_modified,
432                        ..
433                    } => {
434                        // Note that collection/keys is itself a bso, so the
435                        // json body also carries the timestamp. If they aren't
436                        // identical something has screwed up and we should die.
437                        assert_eq!(last_modified, record.envelope.modified);
438                        let state = GlobalState {
439                            config,
440                            collections,
441                            global,
442                            global_timestamp,
443                            keys: record.payload,
444                            keys_timestamp: last_modified,
445                        };
446                        Ok(Ready { state })
447                    }
448                    // If the server doesn't have a `crypto/keys`, start over
449                    // and reupload our `meta/global` and `crypto/keys`.
450                    Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
451                        Ok(FreshStartRequired { config })
452                    }
453                    other => Err(other.create_storage_error()),
454                }
455            }
456
457            // We've got old state that's likely to be OK.
458            // We keep things simple here - if there's evidence of a new/missing
459            // meta/global or new/missing keys we just restart from scratch.
460            WithPreviousState { old_state } => match self.client.fetch_info_collections()? {
461                Sync15ClientResponse::Success {
462                    record: collections,
463                    ..
464                } => Ok(
465                    if self.engine_updates.is_none()
466                        && is_same_timestamp(old_state.global_timestamp, &collections, "meta")
467                        && is_same_timestamp(old_state.keys_timestamp, &collections, "crypto")
468                    {
469                        Ready {
470                            state: GlobalState {
471                                collections,
472                                ..old_state
473                            },
474                        }
475                    } else {
476                        InitialWithConfig {
477                            config: old_state.config,
478                        }
479                    },
480                ),
481                _ => Ok(InitialWithConfig {
482                    config: old_state.config,
483                }),
484            },
485
486            Ready { state } => Ok(Ready { state }),
487
488            FreshStartRequired { config } => {
489                // Wipe the server.
490                info!("Fresh start: wiping remote");
491                self.client.wipe_all_remote()?;
492
493                // Upload a fresh `meta/global`...
494                info!("Uploading meta/global");
495                let computed = compute_engine_states(EngineStateInput {
496                    local_declined: self.pgs.get_declined().iter().cloned().collect(),
497                    user_changes: self.engine_updates.cloned().unwrap_or_default(),
498                    remote: None,
499                });
500                self.pgs
501                    .set_declined(computed.declined.iter().cloned().collect());
502
503                self.changes_needed = Some(computed.changes_needed);
504
505                let new_global = new_global(self.pgs);
506
507                self.client
508                    .put_meta_global(ServerTimestamp::default(), &new_global)?;
509
510                // ...And a fresh `crypto/keys`.
511                let new_keys = CollectionKeys::new_random()?.to_encrypted_payload(self.root_key)?;
512                let bso = OutgoingEncryptedBso::new(Guid::new("keys").into(), new_keys);
513                self.client
514                    .put_crypto_keys(ServerTimestamp::default(), &bso)?;
515
516                // TODO(lina): Can we pass along server timestamps from the PUTs
517                // above, and avoid re-fetching the `m/g` and `c/k` we just
518                // uploaded?
519                // OTOH(mark): restarting the state machine keeps life simple and rare.
520                Ok(InitialWithConfig { config })
521            }
522        }
523    }
524
525    /// Runs through the state machine to the ready state.
526    pub fn run_to_ready(&mut self, state: Option<GlobalState>) -> error::Result<GlobalState> {
527        let mut s = match state {
528            Some(old_state) => WithPreviousState { old_state },
529            None => Initial,
530        };
531        loop {
532            self.interruptee.err_if_interrupted()?;
533            let label = &s.label();
534            trace!("global state: {:?}", label);
535            match s {
536                Ready { state } => {
537                    self.sequence.push(label);
538                    return Ok(state);
539                }
540                // If we already started over once before, we're likely in a
541                // cycle, and should try again later. Intermediate states
542                // aren't a problem, just the initial ones.
543                FreshStartRequired { .. } | WithPreviousState { .. } | Initial => {
544                    if self.sequence.contains(label) {
545                        // Is this really the correct error?
546                        return Err(ErrorKind::SetupRace);
547                    }
548                }
549                _ => {
550                    if !self.allowed_states.contains(label) {
551                        return Err(ErrorKind::SetupRequired);
552                    }
553                }
554            };
555            self.sequence.push(label);
556            s = self.advance(s)?;
557        }
558    }
559}
560
561/// States in the remote setup process.
562/// TODO(lina): Add link once #56 is merged.
563#[derive(Debug)]
564#[allow(clippy::large_enum_variant)]
565enum SetupState {
566    // These "Initial" states are only ever used when starting from scratch.
567    Initial,
568    InitialWithConfig {
569        config: InfoConfiguration,
570    },
571    InitialWithInfo {
572        config: InfoConfiguration,
573        collections: InfoCollections,
574    },
575    InitialWithMetaGlobal {
576        config: InfoConfiguration,
577        collections: InfoCollections,
578        global: MetaGlobalRecord,
579        global_timestamp: ServerTimestamp,
580    },
581    WithPreviousState {
582        old_state: GlobalState,
583    },
584    Ready {
585        state: GlobalState,
586    },
587    FreshStartRequired {
588        config: InfoConfiguration,
589    },
590}
591
592impl SetupState {
593    fn label(&self) -> &'static str {
594        match self {
595            Initial => "Initial",
596            InitialWithConfig { .. } => "InitialWithConfig",
597            InitialWithInfo { .. } => "InitialWithInfo",
598            InitialWithMetaGlobal { .. } => "InitialWithMetaGlobal",
599            Ready { .. } => "Ready",
600            WithPreviousState { .. } => "WithPreviousState",
601            FreshStartRequired { .. } => "FreshStartRequired",
602        }
603    }
604}
605
606/// Whether we should skip fetching an item. Used when we already have timestamps
607/// and want to check if we should reuse our existing state. The state's fairly
608/// cheap to recreate and very bad to use if it is wrong, so we insist on the
609/// *exact* timestamp matching and not a simple "later than" check.
610fn is_same_timestamp(local: ServerTimestamp, collections: &InfoCollections, key: &str) -> bool {
611    collections.get(key).is_some_and(|ts| local == *ts)
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    use crate::bso::{IncomingEncryptedBso, IncomingEnvelope};
619    use interrupt_support::NeverInterrupts;
620
621    struct InMemoryClient {
622        info_configuration: error::Result<Sync15ClientResponse<InfoConfiguration>>,
623        info_collections: error::Result<Sync15ClientResponse<InfoCollections>>,
624        meta_global: error::Result<Sync15ClientResponse<MetaGlobalRecord>>,
625        crypto_keys: error::Result<Sync15ClientResponse<IncomingEncryptedBso>>,
626    }
627
628    impl SetupStorageClient for InMemoryClient {
629        fn fetch_info_configuration(
630            &self,
631        ) -> error::Result<Sync15ClientResponse<InfoConfiguration>> {
632            match &self.info_configuration {
633                Ok(client_response) => Ok(client_response.clone()),
634                Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
635                    status: 500,
636                    route: "test/path".into(),
637                })),
638            }
639        }
640
641        fn fetch_info_collections(&self) -> error::Result<Sync15ClientResponse<InfoCollections>> {
642            match &self.info_collections {
643                Ok(collections) => Ok(collections.clone()),
644                Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
645                    status: 500,
646                    route: "test/path".into(),
647                })),
648            }
649        }
650
651        fn fetch_meta_global(&self) -> error::Result<Sync15ClientResponse<MetaGlobalRecord>> {
652            match &self.meta_global {
653                Ok(global) => Ok(global.clone()),
654                // TODO(lina): Special handling for 404s, we want to ensure we
655                // handle missing keys and other server errors correctly.
656                Err(_) => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
657                    status: 500,
658                    route: "test/path".into(),
659                })),
660            }
661        }
662
663        fn put_meta_global(
664            &self,
665            xius: ServerTimestamp,
666            global: &MetaGlobalRecord,
667        ) -> error::Result<ServerTimestamp> {
668            // Ensure that the meta/global record we uploaded is "fixed up"
669            assert!(DEFAULT_ENGINES
670                .iter()
671                .filter(|e| e.0 != "logins")
672                .all(|&(k, _v)| global.engines.contains_key(k)));
673            assert!(!global.engines.contains_key("logins"));
674            assert_eq!(global.declined, vec!["logins".to_string()]);
675            // return a different timestamp.
676            Ok(ServerTimestamp(xius.0 + 1))
677        }
678
679        fn fetch_crypto_keys(&self) -> error::Result<Sync15ClientResponse<IncomingEncryptedBso>> {
680            match &self.crypto_keys {
681                Ok(Sync15ClientResponse::Success {
682                    status,
683                    record,
684                    last_modified,
685                    route,
686                }) => Ok(Sync15ClientResponse::Success {
687                    status: *status,
688                    record: IncomingEncryptedBso::new(
689                        record.envelope.clone(),
690                        record.payload.clone(),
691                    ),
692                    last_modified: *last_modified,
693                    route: route.clone(),
694                }),
695                // TODO(lina): Same as above, for 404s.
696                _ => Ok(Sync15ClientResponse::Error(ErrorResponse::ServerError {
697                    status: 500,
698                    route: "test/path".into(),
699                })),
700            }
701        }
702
703        fn put_crypto_keys(
704            &self,
705            xius: ServerTimestamp,
706            _keys: &OutgoingEncryptedBso,
707        ) -> error::Result<()> {
708            assert_eq!(xius, ServerTimestamp(888_800));
709            Err(ErrorKind::StorageHttpError(ErrorResponse::ServerError {
710                status: 500,
711                route: "crypto/keys".to_string(),
712            }))
713        }
714
715        fn wipe_all_remote(&self) -> error::Result<()> {
716            Ok(())
717        }
718    }
719
720    #[allow(clippy::unnecessary_wraps)]
721    fn mocked_success_ts<T>(t: T, ts: i64) -> error::Result<Sync15ClientResponse<T>> {
722        Ok(Sync15ClientResponse::Success {
723            status: 200,
724            record: t,
725            last_modified: ServerTimestamp(ts),
726            route: "test/path".into(),
727        })
728    }
729
730    fn mocked_success<T>(t: T) -> error::Result<Sync15ClientResponse<T>> {
731        mocked_success_ts(t, 0)
732    }
733
734    fn mocked_success_keys(
735        keys: CollectionKeys,
736        root_key: &KeyBundle,
737    ) -> error::Result<Sync15ClientResponse<IncomingEncryptedBso>> {
738        let timestamp = keys.timestamp;
739        let payload = keys.to_encrypted_payload(root_key).unwrap();
740        let bso = IncomingEncryptedBso::new(
741            IncomingEnvelope {
742                id: Guid::new("keys"),
743                modified: timestamp,
744                sortindex: None,
745                ttl: None,
746            },
747            payload,
748        );
749        Ok(Sync15ClientResponse::Success {
750            status: 200,
751            record: bso,
752            last_modified: timestamp,
753            route: "test/path".into(),
754        })
755    }
756
757    #[test]
758    fn test_state_machine_ready_from_empty() {
759        nss::ensure_initialized();
760        error_support::init_for_tests();
761        let root_key = KeyBundle::new_random().unwrap();
762        let keys = CollectionKeys {
763            timestamp: ServerTimestamp(123_400),
764            default: KeyBundle::new_random().unwrap(),
765            collections: HashMap::new(),
766        };
767        let mg = MetaGlobalRecord {
768            sync_id: "syncIDAAAAAA".into(),
769            storage_version: 5usize,
770            engines: vec![(
771                "bookmarks",
772                MetaGlobalEngine {
773                    version: 1usize,
774                    sync_id: "syncIDBBBBBB".into(),
775                },
776            )]
777            .into_iter()
778            .map(|(key, value)| (key.to_owned(), value))
779            .collect(),
780            // We ensure that the record we upload doesn't have a logins record.
781            declined: vec!["logins".to_string()],
782        };
783        let client = InMemoryClient {
784            info_configuration: mocked_success(InfoConfiguration::default()),
785            info_collections: mocked_success(InfoCollections::new(
786                vec![("meta", 123_456), ("crypto", 145_000)]
787                    .into_iter()
788                    .map(|(key, value)| (key.to_owned(), ServerTimestamp(value)))
789                    .collect(),
790            )),
791            meta_global: mocked_success_ts(mg, 999_000),
792            crypto_keys: mocked_success_keys(keys, &root_key),
793        };
794        let mut pgs = PersistedGlobalState::V2 { declined: None };
795
796        let mut state_machine =
797            SetupStateMachine::for_full_sync(&client, &root_key, &mut pgs, None, &NeverInterrupts);
798        assert!(
799            state_machine.run_to_ready(None).is_ok(),
800            "Should drive state machine to ready"
801        );
802        assert_eq!(
803            state_machine.sequence,
804            vec![
805                "Initial",
806                "InitialWithConfig",
807                "InitialWithInfo",
808                "InitialWithMetaGlobal",
809                "Ready",
810            ],
811            "Should cycle through all states"
812        );
813    }
814
815    #[test]
816    fn test_from_previous_state_declined() {
817        nss::ensure_initialized();
818        error_support::init_for_tests();
819        // The state-machine sequence where we didn't use the previous state
820        // (ie, where the state machine restarted)
821        let sm_seq_restarted = vec![
822            "WithPreviousState",
823            "InitialWithConfig",
824            "InitialWithInfo",
825            "InitialWithMetaGlobal",
826            "Ready",
827        ];
828        // The state-machine sequence where we used the previous state.
829        let sm_seq_used_previous = vec!["WithPreviousState", "Ready"];
830
831        // do the actual test.
832        fn do_test(
833            client: &dyn SetupStorageClient,
834            root_key: &KeyBundle,
835            pgs: &mut PersistedGlobalState,
836            engine_updates: Option<&HashMap<String, bool>>,
837            old_state: GlobalState,
838            expected_states: &[&str],
839        ) {
840            let mut state_machine = SetupStateMachine::for_full_sync(
841                client,
842                root_key,
843                pgs,
844                engine_updates,
845                &NeverInterrupts,
846            );
847            assert!(
848                state_machine.run_to_ready(Some(old_state)).is_ok(),
849                "Should drive state machine to ready"
850            );
851            assert_eq!(state_machine.sequence, expected_states);
852        }
853
854        // and all the complicated setup...
855        let ts_metaglobal = 123_456;
856        let ts_keys = 145_000;
857        let root_key = KeyBundle::new_random().unwrap();
858        let keys = CollectionKeys {
859            timestamp: ServerTimestamp(ts_keys + 1),
860            default: KeyBundle::new_random().unwrap(),
861            collections: HashMap::new(),
862        };
863        let mg = MetaGlobalRecord {
864            sync_id: "syncIDAAAAAA".into(),
865            storage_version: 5usize,
866            engines: vec![(
867                "bookmarks",
868                MetaGlobalEngine {
869                    version: 1usize,
870                    sync_id: "syncIDBBBBBB".into(),
871                },
872            )]
873            .into_iter()
874            .map(|(key, value)| (key.to_owned(), value))
875            .collect(),
876            // We ensure that the record we upload doesn't have a logins record.
877            declined: vec!["logins".to_string()],
878        };
879        let collections = InfoCollections::new(
880            vec![("meta", ts_metaglobal), ("crypto", ts_keys)]
881                .into_iter()
882                .map(|(key, value)| (key.to_owned(), ServerTimestamp(value)))
883                .collect(),
884        );
885        let client = InMemoryClient {
886            info_configuration: mocked_success(InfoConfiguration::default()),
887            info_collections: mocked_success(collections.clone()),
888            meta_global: mocked_success_ts(mg.clone(), ts_metaglobal),
889            crypto_keys: mocked_success_keys(keys.clone(), &root_key),
890        };
891
892        // First a test where the "previous" global state is OK to reuse.
893        {
894            let mut pgs = PersistedGlobalState::V2 { declined: None };
895            // A "previous" global state.
896            let old_state = GlobalState {
897                config: InfoConfiguration::default(),
898                collections: collections.clone(),
899                global: mg.clone(),
900                global_timestamp: ServerTimestamp(ts_metaglobal),
901                keys: keys
902                    .to_encrypted_payload(&root_key)
903                    .expect("should always work in this test"),
904                keys_timestamp: ServerTimestamp(ts_keys),
905            };
906            do_test(
907                &client,
908                &root_key,
909                &mut pgs,
910                None,
911                old_state,
912                &sm_seq_used_previous,
913            );
914        }
915
916        // Now where the meta/global record on the server is later.
917        {
918            let mut pgs = PersistedGlobalState::V2 { declined: None };
919            // A "previous" global state.
920            let old_state = GlobalState {
921                config: InfoConfiguration::default(),
922                collections: collections.clone(),
923                global: mg.clone(),
924                global_timestamp: ServerTimestamp(999_999),
925                keys: keys
926                    .to_encrypted_payload(&root_key)
927                    .expect("should always work in this test"),
928                keys_timestamp: ServerTimestamp(ts_keys),
929            };
930            do_test(
931                &client,
932                &root_key,
933                &mut pgs,
934                None,
935                old_state,
936                &sm_seq_restarted,
937            );
938        }
939
940        // Where keys on the server is later.
941        {
942            let mut pgs = PersistedGlobalState::V2 { declined: None };
943            // A "previous" global state.
944            let old_state = GlobalState {
945                config: InfoConfiguration::default(),
946                collections: collections.clone(),
947                global: mg.clone(),
948                global_timestamp: ServerTimestamp(ts_metaglobal),
949                keys: keys
950                    .to_encrypted_payload(&root_key)
951                    .expect("should always work in this test"),
952                keys_timestamp: ServerTimestamp(999_999),
953            };
954            do_test(
955                &client,
956                &root_key,
957                &mut pgs,
958                None,
959                old_state,
960                &sm_seq_restarted,
961            );
962        }
963
964        // Where there are engine-state changes.
965        {
966            let mut pgs = PersistedGlobalState::V2 { declined: None };
967            // A "previous" global state.
968            let old_state = GlobalState {
969                config: InfoConfiguration::default(),
970                collections,
971                global: mg,
972                global_timestamp: ServerTimestamp(ts_metaglobal),
973                keys: keys
974                    .to_encrypted_payload(&root_key)
975                    .expect("should always work in this test"),
976                keys_timestamp: ServerTimestamp(ts_keys),
977            };
978            let mut engine_updates = HashMap::<String, bool>::new();
979            engine_updates.insert("logins".to_string(), false);
980            do_test(
981                &client,
982                &root_key,
983                &mut pgs,
984                Some(&engine_updates),
985                old_state,
986                &sm_seq_restarted,
987            );
988            let declined = match pgs {
989                PersistedGlobalState::V2 { declined: d } => d,
990            };
991            // and check we now consider logins as declined.
992            assert_eq!(declined, Some(vec!["logins".to_string()]));
993        }
994    }
995
996    fn string_set(s: &[&str]) -> HashSet<String> {
997        s.iter().map(ToString::to_string).collect()
998    }
999    fn string_map<T: Clone>(s: &[(&str, T)]) -> HashMap<String, T> {
1000        s.iter().map(|v| (v.0.to_string(), v.1.clone())).collect()
1001    }
1002    #[test]
1003    fn test_engine_states() {
1004        assert_eq!(
1005            compute_engine_states(EngineStateInput {
1006                local_declined: string_set(&["foo", "bar"]),
1007                remote: None,
1008                user_changes: Default::default(),
1009            }),
1010            EngineStateOutput {
1011                declined: string_set(&["foo", "bar"]),
1012                // No wipes, no resets
1013                changes_needed: Default::default(),
1014            }
1015        );
1016        assert_eq!(
1017            compute_engine_states(EngineStateInput {
1018                local_declined: string_set(&["foo", "bar"]),
1019                remote: Some(RemoteEngineState {
1020                    declined: string_set(&["foo"]),
1021                    info_collections: string_set(&["bar"])
1022                }),
1023                user_changes: Default::default(),
1024            }),
1025            EngineStateOutput {
1026                // Now we have `foo`.
1027                declined: string_set(&["foo"]),
1028                // No wipes, no resets, should just be a local update.
1029                changes_needed: Default::default(),
1030            }
1031        );
1032        assert_eq!(
1033            compute_engine_states(EngineStateInput {
1034                local_declined: string_set(&["foo", "bar"]),
1035                remote: Some(RemoteEngineState {
1036                    declined: string_set(&["foo", "bar", "quux"]),
1037                    info_collections: string_set(&[])
1038                }),
1039                user_changes: Default::default(),
1040            }),
1041            EngineStateOutput {
1042                // Now we have `foo`.
1043                declined: string_set(&["foo", "bar", "quux"]),
1044                changes_needed: EngineChangesNeeded {
1045                    // Should reset `quux`.
1046                    local_resets: string_set(&["quux"]),
1047                    // No wipes, though.
1048                    remote_wipes: string_set(&[]),
1049                }
1050            }
1051        );
1052        assert_eq!(
1053            compute_engine_states(EngineStateInput {
1054                local_declined: string_set(&["bar", "baz"]),
1055                remote: Some(RemoteEngineState {
1056                    declined: string_set(&["bar", "baz",]),
1057                    info_collections: string_set(&["quux"])
1058                }),
1059                // Change a declined engine to undeclined.
1060                user_changes: string_map(&[("bar", true)]),
1061            }),
1062            EngineStateOutput {
1063                declined: string_set(&["baz"]),
1064                // No wipes, just undecline it.
1065                changes_needed: Default::default()
1066            }
1067        );
1068        assert_eq!(
1069            compute_engine_states(EngineStateInput {
1070                local_declined: string_set(&["bar", "baz"]),
1071                remote: Some(RemoteEngineState {
1072                    declined: string_set(&["bar", "baz"]),
1073                    info_collections: string_set(&["foo"])
1074                }),
1075                // Change an engine which exists remotely to declined.
1076                user_changes: string_map(&[("foo", false)]),
1077            }),
1078            EngineStateOutput {
1079                declined: string_set(&["baz", "bar", "foo"]),
1080                // No wipes, just undecline it.
1081                changes_needed: EngineChangesNeeded {
1082                    // Should reset our local foo
1083                    local_resets: string_set(&["foo"]),
1084                    // And wipe the server.
1085                    remote_wipes: string_set(&["foo"]),
1086                }
1087            }
1088        );
1089    }
1090}