tabs/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::TabsEngine;
6use crate::TabsStore;
7use anyhow::Result;
8use std::sync::Arc;
9use sync15::engine::BridgedEngineAdaptor;
10use sync15::ServerTimestamp;
11
12impl TabsStore {
13    // Returns a bridged sync engine for Desktop for this store.
14    pub fn bridged_engine(self: Arc<Self>) -> Arc<TabsBridgedEngine> {
15        let engine = TabsEngine::new(self);
16        let bridged_engine = TabsBridgedEngineAdaptor { engine };
17        Arc::new(TabsBridgedEngine::new(Box::new(bridged_engine)))
18    }
19}
20
21/// A bridged engine implements all the methods needed to make the
22/// `storage.sync` store work with Desktop's Sync implementation.
23/// Conceptually it's very similar to our SyncEngine and there's a BridgedEngineAdaptor
24/// trait we can implement to get a `BridgedEngine` from a `SyncEngine`, so that's
25/// what we do. See also #2841, which will finally unify them completely.
26struct TabsBridgedEngineAdaptor {
27    engine: TabsEngine,
28}
29
30impl BridgedEngineAdaptor for TabsBridgedEngineAdaptor {
31    fn last_sync(&self) -> Result<i64> {
32        Ok(self.engine.get_last_sync()?.unwrap_or_default().as_millis())
33    }
34
35    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> {
36        self.engine
37            .set_last_sync(ServerTimestamp::from_millis(last_sync_millis))
38    }
39
40    fn engine(&self) -> &dyn sync15::engine::SyncEngine {
41        &self.engine
42    }
43}
44
45// The UniFFI-exposed `TabsBridgedEngine` (a thin newtype around
46// `sync15::engine::BridgedEngineWrapper`) is generated by this macro, which
47// removes the facade + BSO marshalling boilerplate that used to live here.
48// tabs' `set_uploaded` UDL row is `sequence<TabsGuid>` (a custom type over
49// `sync_guid::Guid`), so the id element type is `sync_guid::Guid`.
50sync15::uniffi_bridged_engine!(TabsBridgedEngine, sync_guid::Guid);
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::storage::{RemoteTab, TABS_CLIENT_TTL};
56    use crate::sync::record::TabsRecordTab;
57    use serde_json::json;
58    use std::collections::HashMap;
59    use sync15::{ClientData, DeviceType, RemoteClient};
60
61    // A copy of the normal "engine" tests but which go via the bridge
62    #[test]
63    fn test_sync_via_bridge() {
64        error_support::init_for_tests();
65
66        let store = Arc::new(TabsStore::new_with_mem_path("test-bridge_incoming"));
67
68        // Set some local tabs for our device.
69        let my_tabs = vec![
70            RemoteTab {
71                title: "my first tab".to_string(),
72                url_history: vec!["http://1.com".to_string()],
73                last_used: 2,
74                ..Default::default()
75            },
76            RemoteTab {
77                title: "my second tab".to_string(),
78                url_history: vec!["http://2.com".to_string()],
79                last_used: 1,
80                ..Default::default()
81            },
82        ];
83        store.set_local_tabs(my_tabs.clone());
84
85        let bridge = store.bridged_engine();
86
87        let client_data = ClientData {
88            local_client_id: "my-device".to_string(),
89            recent_clients: HashMap::from([
90                (
91                    "my-device".to_string(),
92                    RemoteClient {
93                        fxa_device_id: None,
94                        device_name: "my device".to_string(),
95                        device_type: sync15::DeviceType::Unknown,
96                    },
97                ),
98                (
99                    "device-no-tabs".to_string(),
100                    RemoteClient {
101                        fxa_device_id: None,
102                        device_name: "device with no tabs".to_string(),
103                        device_type: DeviceType::Unknown,
104                    },
105                ),
106                (
107                    "device-with-a-tab".to_string(),
108                    RemoteClient {
109                        fxa_device_id: None,
110                        device_name: "device with a tab".to_string(),
111                        device_type: DeviceType::Unknown,
112                    },
113                ),
114            ]),
115        };
116        bridge
117            .prepare_for_sync(&serde_json::to_string(&client_data).unwrap())
118            .expect("should work");
119
120        let records = vec![
121            // my-device should be ignored by sync - here it is acting as what our engine last
122            // wrote, but the actual tabs in our store we set above are what should be used.
123            json!({
124                "id": "my-device",
125                "clientName": "my device",
126                "tabs": [{
127                    "title": "the title",
128                    "urlHistory": [
129                        "https://mozilla.org/"
130                    ],
131                    "icon": "https://mozilla.org/icon",
132                    "lastUsed": 1643764207
133                }]
134            }),
135            json!({
136                "id": "device-no-tabs",
137                "clientName": "device with no tabs",
138                "tabs": [],
139            }),
140            json!({
141                "id": "device-with-a-tab",
142                "clientName": "device with a tab",
143                "tabs": [{
144                    "title": "the title",
145                    "urlHistory": [
146                        "https://mozilla.org/"
147                    ],
148                    "icon": "https://mozilla.org/icon",
149                    "lastUsed": 1643764207
150                }]
151            }),
152            // This has the main payload as OK but the tabs part invalid.
153            json!({
154                "id": "device-with-invalid-tab",
155                "clientName": "device with a tab",
156                "tabs": [{
157                    "foo": "bar",
158                }]
159            }),
160            // We want this to be a valid payload but an invalid tab - so it needs an ID.
161            json!({
162                "id": "invalid-tab",
163                "foo": "bar"
164            }),
165        ];
166
167        let mut incoming = Vec::new();
168        for record in records {
169            // Annoyingly we can't use `IncomingEnvelope` directly as it intentionally doesn't
170            // support Serialize - so need to use explicit json.
171            let envelope = json!({
172                "id": record.get("id"),
173                "modified": 0,
174                "payload": serde_json::to_string(&record).unwrap(),
175            });
176            incoming.push(serde_json::to_string(&envelope).unwrap());
177        }
178
179        bridge.store_incoming(incoming).expect("should store");
180
181        let out = bridge.apply().expect("should apply");
182
183        assert_eq!(out.len(), 1);
184        let ours = serde_json::from_str::<serde_json::Value>(&out[0]).unwrap();
185        // As above, can't use `OutgoingEnvelope` as it doesn't Deserialize.
186        // First, convert my_tabs from the local `RemoteTab` to the Sync specific `TabsRecord`
187        let expected_tabs: Vec<TabsRecordTab> = my_tabs.into_iter().map(Into::into).collect();
188        let expected = json!({
189            "id": "my-device".to_string(),
190            "payload": json!({
191                "id": "my-device".to_string(),
192                "clientName": "my device",
193                "tabs": serde_json::to_value(expected_tabs).unwrap(),
194            }).to_string(),
195            "ttl": TABS_CLIENT_TTL,
196        });
197
198        assert_eq!(ours, expected);
199        bridge.set_uploaded(1234, vec![]).unwrap();
200        assert_eq!(bridge.last_sync().unwrap(), 1234);
201    }
202
203    #[test]
204    fn test_sync_meta() {
205        error_support::init_for_tests();
206
207        let store = Arc::new(TabsStore::new_with_mem_path("test-meta"));
208        let bridge = store.bridged_engine();
209
210        // Should not error or panic
211        assert_eq!(bridge.last_sync().unwrap(), 0);
212        bridge.set_last_sync(3).unwrap();
213        assert_eq!(bridge.last_sync().unwrap(), 3);
214
215        assert!(bridge.sync_id().unwrap().is_none());
216
217        bridge.ensure_current_sync_id("some_guid").unwrap();
218        assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
219        // changing the sync ID should reset the timestamp
220        assert_eq!(bridge.last_sync().unwrap(), 0);
221        bridge.set_last_sync(3).unwrap();
222
223        bridge.reset_sync_id().unwrap();
224        // should now be a random guid.
225        assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
226        // should have reset the last sync timestamp.
227        assert_eq!(bridge.last_sync().unwrap(), 0);
228        bridge.set_last_sync(3).unwrap();
229
230        // `reset` clears the guid and the timestamp
231        bridge.reset().unwrap();
232        assert_eq!(bridge.last_sync().unwrap(), 0);
233        assert!(bridge.sync_id().unwrap().is_none());
234    }
235}