1use 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
22const 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
39const DEFAULT_DECLINED: &[&str] = &[];
41
42#[derive(Debug, Serialize, Deserialize)]
54#[serde(tag = "schema_version")]
55pub enum PersistedGlobalState {
56 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 declined: HashSet<String>,
93 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 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 local_resets: set_difference(&result_declined, &input.local_declined),
131 remote_wipes: set_intersection(&info_collections, &must_disable),
136 },
137 declined: result_declined,
138 };
139 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#[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
173fn 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 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: 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 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 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 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 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 global.storage_version > STORAGE_VERSION {
348 return Err(ErrorKind::ClientUpgradeRequired);
349 }
350
351 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 self.pgs
369 .set_declined(result.declined.iter().cloned().collect());
370 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 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 if self.changes_needed.is_some() {
400 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 match self.client.fetch_crypto_keys()? {
429 Sync15ClientResponse::Success {
430 record,
431 last_modified,
432 ..
433 } => {
434 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 Sync15ClientResponse::Error(ErrorResponse::NotFound { .. }) => {
451 Ok(FreshStartRequired { config })
452 }
453 other => Err(other.create_storage_error()),
454 }
455 }
456
457 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 info!("Fresh start: wiping remote");
491 self.client.wipe_all_remote()?;
492
493 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 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 Ok(InitialWithConfig { config })
521 }
522 }
523 }
524
525 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 FreshStartRequired { .. } | WithPreviousState { .. } | Initial => {
544 if self.sequence.contains(label) {
545 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#[derive(Debug)]
564#[allow(clippy::large_enum_variant)]
565enum SetupState {
566 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
606fn 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 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 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 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 _ => 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 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 let sm_seq_restarted = vec![
822 "WithPreviousState",
823 "InitialWithConfig",
824 "InitialWithInfo",
825 "InitialWithMetaGlobal",
826 "Ready",
827 ];
828 let sm_seq_used_previous = vec!["WithPreviousState", "Ready"];
830
831 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 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 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 {
894 let mut pgs = PersistedGlobalState::V2 { declined: None };
895 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 {
918 let mut pgs = PersistedGlobalState::V2 { declined: None };
919 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 {
942 let mut pgs = PersistedGlobalState::V2 { declined: None };
943 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 {
966 let mut pgs = PersistedGlobalState::V2 { declined: None };
967 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 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 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 declined: string_set(&["foo"]),
1028 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 declined: string_set(&["foo", "bar", "quux"]),
1044 changes_needed: EngineChangesNeeded {
1045 local_resets: string_set(&["quux"]),
1047 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 user_changes: string_map(&[("bar", true)]),
1061 }),
1062 EngineStateOutput {
1063 declined: string_set(&["baz"]),
1064 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 user_changes: string_map(&[("foo", false)]),
1077 }),
1078 EngineStateOutput {
1079 declined: string_set(&["baz", "bar", "foo"]),
1080 changes_needed: EngineChangesNeeded {
1082 local_resets: string_set(&["foo"]),
1084 remote_wipes: string_set(&["foo"]),
1086 }
1087 }
1088 );
1089 }
1090}