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;
9
10impl LoginStore {
11    /// Returns a bridged sync engine for Desktop for this store.
12    ///
13    /// Unlike Tabs, constructing a `LoginsSyncEngine` locks the DB and can
14    /// fail, so this is fallible (and exposed as `[Throws]` in the UDL). The
15    /// internal error is surfaced via `anyhow`, which UniFFI maps onto
16    /// `LoginsApiError` through `From<anyhow::Error>`.
17    pub fn bridged_engine(self: Arc<Self>) -> Result<Arc<LoginsBridgedEngine>> {
18        let engine = LoginsSyncEngine::new(self)?;
19        Ok(Arc::new(LoginsBridgedEngine::new(Box::new(engine))))
20    }
21}
22
23// The UniFFI-exposed `LoginsBridgedEngine` (a thin newtype around
24// `sync15::engine::BridgedEngineWrapper`) is generated by this macro, which
25// removes the facade + BSO marshalling boilerplate. The wrapper drives our
26// `LoginsSyncEngine`'s `SyncEngine` impl directly.
27sync15::uniffi_bridged_engine!(LoginsBridgedEngine);
28
29#[cfg(not(feature = "keydb"))]
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use crate::db::test_utils::insert_login;
34    use nss_as::ensure_initialized;
35    use std::collections::HashMap;
36
37    // Exercises the sync-metadata plumbing (last_sync / sync_id / reset) that
38    // Desktop's Sync framework drives through the bridge, mirroring the Tabs
39    // `test_sync_meta` test.
40    #[test]
41    fn test_sync_meta() {
42        ensure_initialized();
43        error_support::init_for_tests();
44
45        let store = Arc::new(LoginStore::new_in_memory());
46        let bridge = store.bridged_engine().expect("should create bridge");
47
48        // Fresh DB: never synced.
49        assert_eq!(bridge.last_sync().unwrap(), 0);
50        bridge.set_uploaded(3, vec![]).unwrap();
51        assert_eq!(bridge.last_sync().unwrap(), 3);
52
53        assert!(bridge.sync_id().unwrap().is_none());
54
55        bridge.ensure_current_sync_id("some_guid").unwrap();
56        assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
57        // changing the sync ID should reset the timestamp
58        assert_eq!(bridge.last_sync().unwrap(), 0);
59        // Advance the engine-owned last_sync
60        bridge.set_uploaded(3, vec![]).unwrap();
61
62        bridge.reset_sync_id().unwrap();
63        // should now be a random guid.
64        assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
65        // should have reset the last sync timestamp.
66        assert_eq!(bridge.last_sync().unwrap(), 0);
67        // Advance the engine-owned last_sync
68        bridge.set_uploaded(3, vec![]).unwrap();
69
70        // `reset` clears the guid and the timestamp
71        bridge.reset().unwrap();
72        assert_eq!(bridge.last_sync().unwrap(), 0);
73        assert!(bridge.sync_id().unwrap().is_none());
74    }
75
76    // A roundtrip through the bridge's data path: stage an incoming remote
77    // login, apply it, and confirm the local-only login comes back out for
78    // upload. Unlike `test_sync_meta`, this exercises the JSON (de)serialization
79    // of BSOs and the staged-incoming `Mutex`. Mirrors the Tabs
80    // `test_sync_via_bridge` test.
81    #[test]
82    fn test_sync_via_bridge() {
83        ensure_initialized();
84        error_support::init_for_tests();
85
86        let store = Arc::new(LoginStore::new_in_memory());
87
88        // A local-only login: nothing on the server knows about it yet, so it
89        // should be uploaded.
90        insert_login(
91            &store.lock_db().unwrap(),
92            "local-only-aaaa",
93            Some("local-password"),
94            None,
95        );
96
97        let bridge = store
98            .clone()
99            .bridged_engine()
100            .expect("should create bridge");
101
102        bridge.sync_started().unwrap();
103
104        // An incoming remote login that isn't known locally. We build the
105        // envelope as raw JSON, exactly as the JS bridge hands it to us.
106        let incoming = vec![serde_json::json!({
107            "id": "remote-only-bbbb",
108            "modified": 0,
109            "payload": serde_json::json!({
110                "id": "remote-only-bbbb",
111                "hostname": "https://remote.example.com",
112                "formSubmitURL": "https://remote.example.com",
113                "username": "remote-user",
114                "password": "remote-password",
115            })
116            .to_string(),
117        })
118        .to_string()];
119        bridge
120            .store_incoming(incoming)
121            .expect("should store incoming");
122
123        // Applying stores the remote record locally and returns the local-only
124        // login for upload.
125        let outgoing = bridge.apply(0).expect("should apply");
126        let changes: HashMap<String, serde_json::Value> = outgoing
127            .into_iter()
128            .map(|s| {
129                let bso: serde_json::Value = serde_json::from_str(&s).unwrap();
130                let payload: serde_json::Value =
131                    serde_json::from_str(bso["payload"].as_str().unwrap()).unwrap();
132                (payload["id"].as_str().unwrap().to_string(), payload)
133            })
134            .collect();
135
136        // Only the local login is outgoing; the just-applied remote one is not
137        // re-uploaded.
138        assert_eq!(changes.len(), 1);
139        assert_eq!(changes["local-only-aaaa"]["password"], "local-password");
140
141        // The incoming remote login was actually persisted.
142        let stored = store
143            .get("remote-only-bbbb")
144            .unwrap()
145            .expect("remote login should have been stored");
146        assert_eq!(stored.password, "remote-password");
147
148        // Acknowledging the upload advances last_sync.
149        bridge
150            .set_uploaded(1234, vec!["local-only-aaaa".to_string()])
151            .unwrap();
152        bridge.sync_finished().unwrap();
153        assert_eq!(bridge.last_sync().unwrap(), 1234);
154    }
155}