/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::db::models::address::InternalAddress; use crate::sync::engine::ConfigSyncEngine; use crate::Store; use anyhow::Result; use std::sync::Arc; use sync15::engine::BridgedEngineAdaptor; impl Store { /// Returns a bridged sync engine for addresses, for use by Desktop's Sync /// framework. Constructing a `ConfigSyncEngine` only assembles structs and /// never touches the DB, so this cannot fail. pub fn addresses_bridged_engine(self: Arc) -> Arc { let engine = crate::sync::address::create_engine(self); Arc::new(AddressesBridgedEngine::new(Box::new( AddressesBridgedEngineAdaptor { engine }, ))) } } /// `ConfigSyncEngine` implements `sync15::SyncEngine`, which is what the sync /// manager drives. Desktop instead speaks `mozIBridgedSyncEngine`, whose Rust /// shape is `sync15::BridgedEngine`. The two differ only in that the bridge owns /// the last-sync timestamp explicitly, so this adaptor supplies that and the /// blanket `impl BridgedEngine for A` provides the rest. struct AddressesBridgedEngineAdaptor { engine: ConfigSyncEngine, } impl BridgedEngineAdaptor for AddressesBridgedEngineAdaptor { fn last_sync(&self) -> Result { Ok(self.engine.get_last_sync_millis()?) } fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> { self.engine.set_last_sync_millis(last_sync_millis)?; Ok(()) } fn engine(&self) -> &dyn sync15::engine::SyncEngine { &self.engine } } // Generates the UniFFI-exposed `AddressesBridgedEngine`, a newtype around // `sync15::engine::BridgedEngineWrapper`. The UDL's `set_uploaded` takes // `sequence`, hence the `String` id type. sync15::uniffi_bridged_engine!(AddressesBridgedEngine, String); #[cfg(test)] mod tests { use super::*; use crate::db::models::address::UpdatableAddressFields; use std::collections::HashMap; // Exercises the sync metadata the bridge owns: last_sync, sync_id and reset. #[test] fn test_sync_meta() { error_support::init_for_tests(); let store = Arc::new(Store::new_shared_memory("addresses-bridge").unwrap()); let bridge = store.addresses_bridged_engine(); // Fresh DB: never synced. assert_eq!(bridge.last_sync().unwrap(), 0); bridge.set_last_sync(3).unwrap(); assert_eq!(bridge.last_sync().unwrap(), 3); assert!(bridge.sync_id().unwrap().is_none()); bridge.ensure_current_sync_id("some_guid").unwrap(); assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string())); // changing the sync ID resets the timestamp assert_eq!(bridge.last_sync().unwrap(), 0); bridge.set_last_sync(3).unwrap(); bridge.reset_sync_id().unwrap(); assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string())); assert_eq!(bridge.last_sync().unwrap(), 0); bridge.set_last_sync(3).unwrap(); // `reset` clears the guid and the timestamp. bridge.reset().unwrap(); assert_eq!(bridge.last_sync().unwrap(), 0); assert!(bridge.sync_id().unwrap().is_none()); } // A roundtrip through the bridge's data path: stage an incoming remote // address, apply it, and confirm the local-only address comes back out for // upload. Unlike `test_sync_meta` this exercises the JSON (de)serialization // of BSOs and the sync staging tables, mirroring the logins and tabs // `test_sync_via_bridge` tests. #[test] fn test_sync_via_bridge() { error_support::init_for_tests(); let store = Arc::new(Store::new_shared_memory("addresses-bridge-roundtrip").unwrap()); // A local-only address: nothing on the server knows about it yet, so it // should be uploaded. let local = store .add_address(UpdatableAddressFields { name: "Local Person".to_string(), street_address: "1 Local Lane".to_string(), address_level2: "Seattle, WA".to_string(), country: "US".to_string(), ..Default::default() }) .expect("should add local address"); let bridge = store.clone().addresses_bridged_engine(); // `prepare_for_sync` is what creates the sync staging tables; the client // data it is given is unused by this engine. bridge .prepare_for_sync(r#"{"local_client_id":"my-client","recent_clients":{}}"#) .expect("should prepare for sync"); bridge.sync_started().unwrap(); // An incoming remote address that isn't known locally. We build the // envelope as raw JSON, exactly as the JS bridge hands it to us. let incoming = vec![serde_json::json!({ "id": "remote-only-bbbb", "modified": 0, "payload": serde_json::json!({ "id": "remote-only-bbbb", "entry": { "name": "Remote Person", "street-address": "99 Remote Road", "address-level2": "Portland, OR", "country": "US", "version": 1, }, }) .to_string(), }) .to_string()]; bridge .store_incoming(incoming) .expect("should store incoming"); // Applying stores the remote record locally and returns the local-only // address for upload. let outgoing = bridge.apply().expect("should apply"); let changes: HashMap = outgoing .into_iter() .map(|s| { let bso: serde_json::Value = serde_json::from_str(&s).unwrap(); let payload: serde_json::Value = serde_json::from_str(bso["payload"].as_str().unwrap()).unwrap(); (payload["id"].as_str().unwrap().to_string(), payload) }) .collect(); // Only the local address is outgoing; the just-applied remote one is not // re-uploaded. assert_eq!(changes.len(), 1); assert_eq!( changes[&local.guid]["entry"]["street-address"], "1 Local Lane" ); // The incoming remote address was actually persisted. let stored = store .get_address("remote-only-bbbb".to_string()) .expect("remote address should have been stored"); assert_eq!(stored.street_address, "99 Remote Road"); // `apply` deliberately stamps last_sync with 0 - Desktop applies without // telling us the server timestamp and sends it separately afterwards. assert_eq!(bridge.last_sync().unwrap(), 0); bridge.set_uploaded(1234, vec![local.guid.clone()]).unwrap(); bridge.sync_finished().unwrap(); assert_eq!(bridge.last_sync().unwrap(), 1234); // Acknowledging the upload cleared the record's change counter, so a // subsequent sync has nothing to send. assert!(bridge.apply().expect("should apply again").is_empty()); } }