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