logins/sync/
bridge.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 crate::sync::engine::LoginsSyncEngine;
6use crate::LoginStore;
7use anyhow::Result;
8use std::sync::Arc;
9use sync15::engine::BridgedEngineAdaptor;
10use sync15::ServerTimestamp;
11
12impl LoginStore {
13    /// Returns a bridged sync engine for Desktop for this store.
14    ///
15    /// Unlike Tabs, constructing a `LoginsSyncEngine` locks the DB and can
16    /// fail, so this is fallible (and exposed as `[Throws]` in the UDL). The
17    /// internal error is surfaced via `anyhow`, which UniFFI maps onto
18    /// `LoginsApiError` through `From<anyhow::Error>`.
19    pub fn bridged_engine(self: Arc<Self>) -> Result<Arc<LoginsBridgedEngine>> {
20        let engine = LoginsSyncEngine::new(self)?;
21        let bridged_engine = LoginsBridgedEngineAdaptor { engine };
22        Ok(Arc::new(LoginsBridgedEngine::new(Box::new(bridged_engine))))
23    }
24}
25
26/// `LoginsSyncEngine` only implements the internal `sync15::SyncEngine` trait,
27/// which is what the mobile (Android/iOS) sync manager drives. Desktop's Sync
28/// framework instead speaks the `mozIBridgedSyncEngine` interface, whose Rust
29/// shape is `sync15::BridgedEngine`. This adaptor wraps our `SyncEngine` and,
30/// via the blanket `impl<A: BridgedEngineAdaptor> BridgedEngine for A`, gives
31/// us a `BridgedEngine` for free. The adaptor exists only because these two
32/// sync-engine traits still live side by side; it can go away if they're ever
33/// unified.
34struct LoginsBridgedEngineAdaptor {
35    engine: LoginsSyncEngine,
36}
37
38/// see sync15/src/engine/bridged_engine.rs for required functions for the trait
39impl BridgedEngineAdaptor for LoginsBridgedEngineAdaptor {
40    fn last_sync(&self) -> Result<i64> {
41        // `get_last_sync` takes the `&LoginDb` to avoid deadlocking when called
42        // mid-sync (while the lock is already held). The bridge methods are
43        // always called outside a sync transaction, so we can lock here.
44        let db = self.engine.store.lock_db()?;
45        Ok(self
46            .engine
47            .get_last_sync(&db)?
48            .unwrap_or_default()
49            .as_millis())
50    }
51
52    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> {
53        let db = self.engine.store.lock_db()?;
54        self.engine
55            .set_last_sync(&db, ServerTimestamp::from_millis(last_sync_millis))?;
56        Ok(())
57    }
58
59    fn engine(&self) -> &dyn sync15::engine::SyncEngine {
60        &self.engine
61    }
62}
63
64// The UniFFI-exposed `LoginsBridgedEngine` (a thin newtype around
65// `sync15::engine::BridgedEngineWrapper`) is generated by this macro, which
66// removes the facade + BSO marshalling boilerplate that used to live here.
67// logins' `set_uploaded` UDL row is `sequence<string>`, so the id element type
68// is `String`. See services/interfaces/mozIBridgedSyncEngine.idl for the contract.
69sync15::uniffi_bridged_engine!(LoginsBridgedEngine, String);
70
71#[cfg(not(feature = "keydb"))]
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::db::test_utils::insert_login;
76    use nss_as::ensure_initialized;
77    use std::collections::HashMap;
78
79    // Exercises the sync-metadata plumbing (last_sync / sync_id / reset) that
80    // Desktop's Sync framework drives through the bridge, mirroring the Tabs
81    // `test_sync_meta` test.
82    #[test]
83    fn test_sync_meta() {
84        ensure_initialized();
85        error_support::init_for_tests();
86
87        let store = Arc::new(LoginStore::new_in_memory());
88        let bridge = store.bridged_engine().expect("should create bridge");
89
90        // Fresh DB: never synced.
91        assert_eq!(bridge.last_sync().unwrap(), 0);
92        bridge.set_last_sync(3).unwrap();
93        assert_eq!(bridge.last_sync().unwrap(), 3);
94
95        assert!(bridge.sync_id().unwrap().is_none());
96
97        bridge.ensure_current_sync_id("some_guid").unwrap();
98        assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
99        // changing the sync ID should reset the timestamp
100        assert_eq!(bridge.last_sync().unwrap(), 0);
101        bridge.set_last_sync(3).unwrap();
102
103        bridge.reset_sync_id().unwrap();
104        // should now be a random guid.
105        assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
106        // should have reset the last sync timestamp.
107        assert_eq!(bridge.last_sync().unwrap(), 0);
108        bridge.set_last_sync(3).unwrap();
109
110        // `reset` clears the guid and the timestamp
111        bridge.reset().unwrap();
112        assert_eq!(bridge.last_sync().unwrap(), 0);
113        assert!(bridge.sync_id().unwrap().is_none());
114    }
115
116    // A roundtrip through the bridge's data path: stage an incoming remote
117    // login, apply it, and confirm the local-only login comes back out for
118    // upload. Unlike `test_sync_meta`, this exercises the JSON (de)serialization
119    // of BSOs and the staged-incoming `Mutex`. Mirrors the Tabs
120    // `test_sync_via_bridge` test.
121    #[test]
122    fn test_sync_via_bridge() {
123        ensure_initialized();
124        error_support::init_for_tests();
125
126        let store = Arc::new(LoginStore::new_in_memory());
127
128        // A local-only login: nothing on the server knows about it yet, so it
129        // should be uploaded.
130        insert_login(
131            &store.lock_db().unwrap(),
132            "local-only-aaaa",
133            Some("local-password"),
134            None,
135        );
136
137        let bridge = store
138            .clone()
139            .bridged_engine()
140            .expect("should create bridge");
141
142        bridge.sync_started().unwrap();
143
144        // An incoming remote login that isn't known locally. We build the
145        // envelope as raw JSON, exactly as the JS bridge hands it to us.
146        let incoming = vec![serde_json::json!({
147            "id": "remote-only-bbbb",
148            "modified": 0,
149            "payload": serde_json::json!({
150                "id": "remote-only-bbbb",
151                "hostname": "https://remote.example.com",
152                "formSubmitURL": "https://remote.example.com",
153                "username": "remote-user",
154                "password": "remote-password",
155            })
156            .to_string(),
157        })
158        .to_string()];
159        bridge
160            .store_incoming(incoming)
161            .expect("should store incoming");
162
163        // Applying stores the remote record locally and returns the local-only
164        // login for upload.
165        let outgoing = bridge.apply().expect("should apply");
166        let changes: HashMap<String, serde_json::Value> = outgoing
167            .into_iter()
168            .map(|s| {
169                let bso: serde_json::Value = serde_json::from_str(&s).unwrap();
170                let payload: serde_json::Value =
171                    serde_json::from_str(bso["payload"].as_str().unwrap()).unwrap();
172                (payload["id"].as_str().unwrap().to_string(), payload)
173            })
174            .collect();
175
176        // Only the local login is outgoing; the just-applied remote one is not
177        // re-uploaded.
178        assert_eq!(changes.len(), 1);
179        assert_eq!(changes["local-only-aaaa"]["password"], "local-password");
180
181        // The incoming remote login was actually persisted.
182        let stored = store
183            .get("remote-only-bbbb")
184            .unwrap()
185            .expect("remote login should have been stored");
186        assert_eq!(stored.password, "remote-password");
187
188        // Acknowledging the upload advances last_sync.
189        bridge
190            .set_uploaded(1234, vec!["local-only-aaaa".to_string()])
191            .unwrap();
192        bridge.sync_finished().unwrap();
193        assert_eq!(bridge.last_sync().unwrap(), 1234);
194    }
195}