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}