autofill/sync/credit_card/
outgoing.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*/
5
6use crate::db::models::credit_card::InternalCreditCard;
7use crate::db::schema::CREDIT_CARD_COMMON_COLS;
8use crate::encryption::EncryptorDecryptor;
9use crate::error::*;
10use crate::sync::common::*;
11use crate::sync::{credit_card::CreditCardPayload, OutgoingBso, ProcessOutgoingRecordImpl};
12use rusqlite::{Row, Transaction};
13use sync_guid::Guid as SyncGuid;
14
15const DATA_TABLE_NAME: &str = "credit_cards_data";
16const MIRROR_TABLE_NAME: &str = "credit_cards_mirror";
17const STAGING_TABLE_NAME: &str = "credit_cards_sync_outgoing_staging";
18
19pub(super) struct OutgoingCreditCardsImpl {
20    pub(super) encdec: EncryptorDecryptor,
21}
22
23impl ProcessOutgoingRecordImpl for OutgoingCreditCardsImpl {
24    type Record = InternalCreditCard;
25
26    /// Gets the local records that have unsynced changes or don't have corresponding mirror
27    /// records and upserts them to the mirror table
28    fn fetch_outgoing_records(&self, tx: &Transaction<'_>) -> anyhow::Result<Vec<OutgoingBso>> {
29        let data_sql = format!(
30            "SELECT
31                l.{common_cols},
32                m.payload,
33                l.sync_change_counter
34            FROM credit_cards_data l
35            LEFT JOIN credit_cards_mirror m
36            ON l.guid = m.guid
37            WHERE
38                l.cc_number_enc <> ''
39            AND
40                (
41                    sync_change_counter > 0 OR
42                    l.guid NOT IN (
43                        SELECT m.guid
44                        FROM credit_cards_mirror m
45                    )
46                )",
47            common_cols = CREDIT_CARD_COMMON_COLS,
48        );
49        let record_from_data_row: &dyn Fn(&Row<'_>) -> Result<(OutgoingBso, i64)> = &|row| {
50            let mut record = InternalCreditCard::from_row(row)?.into_payload(&self.encdec)?;
51            // If the server had unknown fields we fetch it and add it to the record
52            if let Some(enc_s) = row.get::<_, Option<String>>("payload")? {
53                // The full payload in the credit cards mirror is encrypted
54                let mirror_payload: CreditCardPayload =
55                    serde_json::from_str(&self.encdec.decrypt(&enc_s)?)?;
56                record.entry.unknown_fields = mirror_payload.entry.unknown_fields;
57            };
58
59            Ok((
60                OutgoingBso::from_content_with_id(record)?,
61                row.get::<_, i64>("sync_change_counter")?,
62            ))
63        };
64
65        let tombstones_sql = "SELECT guid FROM credit_cards_tombstones";
66
67        // save outgoing records to the mirror table
68        let staging_records = common_get_outgoing_staging_records(
69            tx,
70            &data_sql,
71            tombstones_sql,
72            record_from_data_row,
73        )?
74        .into_iter()
75        .map(|(bso, change_counter)| {
76            // Turn the record into an encrypted repr to save in the mirror.
77            let encrypted = self.encdec.encrypt(&bso.payload)?;
78            Ok((bso.envelope.id, encrypted, change_counter))
79        })
80        .collect::<Result<_>>()?;
81        common_save_outgoing_records(tx, STAGING_TABLE_NAME, staging_records)?;
82
83        // return outgoing changes
84        Ok(
85            common_get_outgoing_records(tx, &data_sql, tombstones_sql, record_from_data_row)?
86                .into_iter()
87                .map(|(bso, _change_counter)| bso)
88                .collect::<Vec<OutgoingBso>>(),
89        )
90    }
91
92    fn finish_synced_items(
93        &self,
94        tx: &Transaction<'_>,
95        records_synced: Vec<SyncGuid>,
96    ) -> anyhow::Result<()> {
97        common_finish_synced_items(
98            tx,
99            DATA_TABLE_NAME,
100            MIRROR_TABLE_NAME,
101            STAGING_TABLE_NAME,
102            records_synced,
103        )?;
104
105        Ok(())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::db::credit_cards::{add_internal_credit_card, tests::test_insert_mirror_record};
113    use crate::sync::{common::tests::*, test::new_syncable_mem_db, UnknownFields};
114    use serde_json::{json, Map, Value};
115    use types::Timestamp;
116
117    lazy_static::lazy_static! {
118        static ref TEST_JSON_RECORDS: Map<String, Value> = {
119            // NOTE: the JSON here is the same as stored on the sync server -
120            // the superfluous `entry` is unfortunate but from desktop.
121            let val = json! {{
122                "C" : {
123                    "id": expand_test_guid('C'),
124                    "entry": {
125                        "cc-name": "Mr Me Another Person",
126                        "cc-number": "8765432112345678",
127                        "cc-exp-month": 1,
128                        "cc-exp-year": 2020,
129                        "cc-type": "visa",
130                        "timeCreated": 0,
131                        "timeLastUsed": 0,
132                        "timeLastModified": 0,
133                        "timesUsed": 0,
134                        "version": 3,
135                    }
136                },
137                "D" : {
138                    "id": expand_test_guid('D'),
139                    "entry": {
140                        "cc-name": "Mr Me Another Person",
141                        "cc-number": "8765432112345678",
142                        "cc-exp-month": 1,
143                        "cc-exp-year": 2020,
144                        "cc-type": "visa",
145                        "timeCreated": 0,
146                        "timeLastUsed": 0,
147                        "timeLastModified": 0,
148                        "timesUsed": 0,
149                        "version": 3,
150                        // Fields we don't understand from the server
151                        "foo": "bar",
152                        "baz": "qux",
153                    }
154                }
155            }};
156            val.as_object().expect("literal is an object").clone()
157        };
158    }
159
160    fn test_json_record(guid_prefix: char) -> Value {
161        TEST_JSON_RECORDS
162            .get(&guid_prefix.to_string())
163            .expect("should exist")
164            .clone()
165    }
166
167    fn test_record(guid_prefix: char, encdec: &EncryptorDecryptor) -> InternalCreditCard {
168        let json = test_json_record(guid_prefix);
169        let payload = serde_json::from_value(json).unwrap();
170        InternalCreditCard::from_payload(payload, encdec).expect("should be valid")
171    }
172
173    #[test]
174    fn test_outgoing_never_synced() {
175        let mut db = new_syncable_mem_db();
176        let tx = db.transaction().expect("should get tx");
177        let co = OutgoingCreditCardsImpl {
178            encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
179        };
180        let test_record = test_record('C', &co.encdec);
181
182        // create date record
183        assert!(add_internal_credit_card(&tx, &test_record).is_ok());
184        do_test_outgoing_never_synced(
185            &tx,
186            &co,
187            &test_record.guid,
188            DATA_TABLE_NAME,
189            MIRROR_TABLE_NAME,
190            STAGING_TABLE_NAME,
191        );
192    }
193
194    #[test]
195    fn test_outgoing_tombstone() {
196        let mut db = new_syncable_mem_db();
197        let tx = db.transaction().expect("should get tx");
198        let co = OutgoingCreditCardsImpl {
199            encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
200        };
201        let test_record = test_record('C', &co.encdec);
202
203        // create tombstone record
204        assert!(tx
205            .execute(
206                "INSERT INTO credit_cards_tombstones (
207                    guid,
208                    time_deleted
209                ) VALUES (
210                    :guid,
211                    :time_deleted
212                )",
213                rusqlite::named_params! {
214                    ":guid": test_record.guid,
215                    ":time_deleted": Timestamp::now(),
216                },
217            )
218            .is_ok());
219        do_test_outgoing_tombstone(
220            &tx,
221            &co,
222            &test_record.guid,
223            DATA_TABLE_NAME,
224            MIRROR_TABLE_NAME,
225            STAGING_TABLE_NAME,
226        );
227    }
228
229    #[test]
230    fn test_outgoing_synced_with_local_change() {
231        let mut db = new_syncable_mem_db();
232        let tx = db.transaction().expect("should get tx");
233        let co = OutgoingCreditCardsImpl {
234            encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
235        };
236
237        // create synced record with non-zero sync_change_counter
238        let mut test_record = test_record('C', &co.encdec);
239        let initial_change_counter_val = 2;
240        test_record.metadata.sync_change_counter = initial_change_counter_val;
241        assert!(add_internal_credit_card(&tx, &test_record).is_ok());
242        let guid = test_record.guid.clone();
243        //test_insert_mirror_record doesn't encrypt the mirror payload, but in reality we do
244        // so we encrypt here so our fetch_outgoing_records doesn't break
245        let mut bso = test_record.into_test_incoming_bso(&co.encdec, Default::default());
246        bso.payload = co.encdec.encrypt(&bso.payload).unwrap();
247        test_insert_mirror_record(&tx, bso);
248        exists_with_counter_value_in_table(&tx, DATA_TABLE_NAME, &guid, initial_change_counter_val);
249
250        do_test_outgoing_synced_with_local_change(
251            &tx,
252            &co,
253            &guid,
254            DATA_TABLE_NAME,
255            MIRROR_TABLE_NAME,
256            STAGING_TABLE_NAME,
257        );
258    }
259
260    #[test]
261    fn test_outgoing_synced_with_no_change() {
262        let mut db = new_syncable_mem_db();
263        let tx = db.transaction().expect("should get tx");
264        let co = OutgoingCreditCardsImpl {
265            encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
266        };
267
268        // create synced record with no changes (sync_change_counter = 0)
269        let test_record = test_record('C', &co.encdec);
270        let guid = test_record.guid.clone();
271        assert!(add_internal_credit_card(&tx, &test_record).is_ok());
272        test_insert_mirror_record(
273            &tx,
274            test_record.into_test_incoming_bso(&co.encdec, Default::default()),
275        );
276
277        do_test_outgoing_synced_with_no_change(
278            &tx,
279            &co,
280            &guid,
281            DATA_TABLE_NAME,
282            STAGING_TABLE_NAME,
283        );
284    }
285
286    #[test]
287    fn test_outgoing_roundtrip_unknown() {
288        let mut db = new_syncable_mem_db();
289        let tx = db.transaction().expect("should get tx");
290        let co = OutgoingCreditCardsImpl {
291            encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
292        };
293
294        // create synced record with non-zero sync_change_counter
295        let mut test_record = test_record('D', &co.encdec);
296        let initial_change_counter_val = 2;
297        test_record.metadata.sync_change_counter = initial_change_counter_val;
298        assert!(add_internal_credit_card(&tx, &test_record).is_ok());
299
300        let unknown_fields: UnknownFields =
301            serde_json::from_value(json! {{ "foo": "bar", "baz": "qux"}}).unwrap();
302
303        //test_insert_mirror_record doesn't encrypt the mirror payload, but in reality we do
304        // so we encrypt here so our fetch_outgoing_records doesn't break
305        let mut bso = test_record
306            .clone()
307            .into_test_incoming_bso(&co.encdec, unknown_fields);
308        bso.payload = co.encdec.encrypt(&bso.payload).unwrap();
309        test_insert_mirror_record(&tx, bso);
310        exists_with_counter_value_in_table(
311            &tx,
312            DATA_TABLE_NAME,
313            &test_record.guid,
314            initial_change_counter_val,
315        );
316
317        let outgoing = &co.fetch_outgoing_records(&tx).unwrap();
318        // Unknown fields are: {"foo": "bar", "baz": "qux"}
319        // Ensure we have our unknown values for the roundtrip
320        let bso_payload: Map<String, Value> = serde_json::from_str(&outgoing[0].payload).unwrap();
321        let entry = bso_payload.get("entry").unwrap();
322        assert_eq!(entry.get("foo").unwrap(), "bar");
323        assert_eq!(entry.get("baz").unwrap(), "qux");
324        do_test_outgoing_synced_with_local_change(
325            &tx,
326            &co,
327            &test_record.guid,
328            DATA_TABLE_NAME,
329            MIRROR_TABLE_NAME,
330            STAGING_TABLE_NAME,
331        );
332    }
333}