autofill/sync/credit_card/
outgoing.rs
1use 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 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 let Some(enc_s) = row.get::<_, Option<String>>("payload")? {
53 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 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 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 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 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 "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 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 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 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 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 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 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 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 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}