autofill/db/
credit_cards.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::{
7    models::{
8        credit_card::{InternalCreditCard, UpdatableCreditCardFields},
9        Metadata,
10    },
11    schema::{CREDIT_CARD_COMMON_COLS, CREDIT_CARD_COMMON_VALS},
12};
13use crate::error::*;
14
15use rusqlite::{Connection, Transaction};
16use sync_guid::Guid;
17use types::Timestamp;
18
19pub(crate) fn add_credit_card(
20    conn: &Connection,
21    new_credit_card_fields: UpdatableCreditCardFields,
22) -> Result<InternalCreditCard> {
23    let now = Timestamp::now();
24
25    // We return an InternalCreditCard, so set it up first, including the
26    // missing fields, before we insert it.
27    let credit_card = InternalCreditCard {
28        guid: Guid::random(),
29        cc_name: new_credit_card_fields.cc_name,
30        cc_number_enc: new_credit_card_fields.cc_number_enc,
31        cc_number_last_4: new_credit_card_fields.cc_number_last_4,
32        cc_exp_month: new_credit_card_fields.cc_exp_month,
33        cc_exp_year: new_credit_card_fields.cc_exp_year,
34        // Credit card types are a fixed set of strings as defined in the link below
35        // (https://searchfox.org/mozilla-central/rev/7ef5cefd0468b8f509efe38e0212de2398f4c8b3/toolkit/modules/CreditCard.jsm#9-22)
36        cc_type: new_credit_card_fields.cc_type,
37        metadata: Metadata {
38            time_created: now,
39            time_last_modified: now,
40            ..Default::default()
41        },
42    };
43
44    let tx = conn.unchecked_transaction()?;
45    add_internal_credit_card(&tx, &credit_card)?;
46    tx.commit()?;
47    Ok(credit_card)
48}
49
50pub(crate) fn add_internal_credit_card(
51    tx: &Transaction<'_>,
52    card: &InternalCreditCard,
53) -> Result<()> {
54    tx.execute(
55        &format!(
56            "INSERT INTO credit_cards_data (
57                {common_cols},
58                sync_change_counter
59            ) VALUES (
60                {common_vals},
61                :sync_change_counter
62            )",
63            common_cols = CREDIT_CARD_COMMON_COLS,
64            common_vals = CREDIT_CARD_COMMON_VALS,
65        ),
66        rusqlite::named_params! {
67            ":guid": card.guid,
68            ":cc_name": card.cc_name,
69            ":cc_number_enc": card.cc_number_enc,
70            ":cc_number_last_4": card.cc_number_last_4,
71            ":cc_exp_month": card.cc_exp_month,
72            ":cc_exp_year": card.cc_exp_year,
73            ":cc_type": card.cc_type,
74            ":time_created": card.metadata.time_created,
75            ":time_last_used": card.metadata.time_last_used,
76            ":time_last_modified": card.metadata.time_last_modified,
77            ":times_used": card.metadata.times_used,
78            ":sync_change_counter": card.metadata.sync_change_counter,
79        },
80    )?;
81    Ok(())
82}
83
84pub(crate) fn get_credit_card(conn: &Connection, guid: &Guid) -> Result<InternalCreditCard> {
85    let sql = format!(
86        "SELECT
87            {common_cols},
88            sync_change_counter
89        FROM credit_cards_data
90        WHERE guid = :guid",
91        common_cols = CREDIT_CARD_COMMON_COLS
92    );
93
94    conn.query_row(&sql, [guid], InternalCreditCard::from_row)
95        .map_err(|e| match e {
96            rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()),
97            e => e.into(),
98        })
99}
100
101pub(crate) fn get_all_credit_cards(conn: &Connection) -> Result<Vec<InternalCreditCard>> {
102    let sql = format!(
103        "SELECT
104            {common_cols},
105            sync_change_counter
106        FROM credit_cards_data",
107        common_cols = CREDIT_CARD_COMMON_COLS
108    );
109
110    let mut stmt = conn.prepare(&sql)?;
111    let credit_cards = stmt
112        .query_map([], InternalCreditCard::from_row)?
113        .collect::<std::result::Result<Vec<InternalCreditCard>, _>>()?;
114    Ok(credit_cards)
115}
116
117pub fn update_credit_card(
118    conn: &Connection,
119    guid: &Guid,
120    credit_card: &UpdatableCreditCardFields,
121) -> Result<()> {
122    let tx = conn.unchecked_transaction()?;
123    tx.execute(
124        "UPDATE credit_cards_data
125        SET cc_name                     = :cc_name,
126            cc_number_enc               = :cc_number_enc,
127            cc_number_last_4            = :cc_number_last_4,
128            cc_exp_month                = :cc_exp_month,
129            cc_exp_year                 = :cc_exp_year,
130            cc_type                     = :cc_type,
131            time_last_modified          = :time_last_modified,
132            sync_change_counter         = sync_change_counter + 1
133        WHERE guid                      = :guid",
134        rusqlite::named_params! {
135            ":cc_name": credit_card.cc_name,
136            ":cc_number_enc": credit_card.cc_number_enc,
137            ":cc_number_last_4": credit_card.cc_number_last_4,
138            ":cc_exp_month": credit_card.cc_exp_month,
139            ":cc_exp_year": credit_card.cc_exp_year,
140            ":cc_type": credit_card.cc_type,
141            ":time_last_modified": Timestamp::now(),
142            ":guid": guid,
143        },
144    )?;
145
146    tx.commit()?;
147    Ok(())
148}
149
150/// Updates all fields including metadata - although the change counter gets
151/// slightly special treatment (eg, when called by Sync we don't want the
152/// change counter incremented).
153pub(crate) fn update_internal_credit_card(
154    tx: &Transaction<'_>,
155    card: &InternalCreditCard,
156    flag_as_changed: bool,
157) -> Result<()> {
158    let change_counter_increment = flag_as_changed as u32; // will be 1 or 0
159    tx.execute(
160        "UPDATE credit_cards_data
161        SET cc_name                     = :cc_name,
162            cc_number_enc               = :cc_number_enc,
163            cc_number_last_4            = :cc_number_last_4,
164            cc_exp_month                = :cc_exp_month,
165            cc_exp_year                 = :cc_exp_year,
166            cc_type                     = :cc_type,
167            time_created                = :time_created,
168            time_last_used              = :time_last_used,
169            time_last_modified          = :time_last_modified,
170            times_used                  = :times_used,
171            sync_change_counter         = sync_change_counter + :change_incr
172        WHERE guid                      = :guid",
173        rusqlite::named_params! {
174            ":cc_name": card.cc_name,
175            ":cc_number_enc": card.cc_number_enc,
176            ":cc_number_last_4": card.cc_number_last_4,
177            ":cc_exp_month": card.cc_exp_month,
178            ":cc_exp_year": card.cc_exp_year,
179            ":cc_type": card.cc_type,
180            ":time_created": card.metadata.time_created,
181            ":time_last_used": card.metadata.time_last_used,
182            ":time_last_modified": card.metadata.time_last_modified,
183            ":times_used": card.metadata.times_used,
184            ":change_incr": change_counter_increment,
185            ":guid": card.guid,
186        },
187    )?;
188    Ok(())
189}
190
191pub fn delete_credit_card(conn: &Connection, guid: &Guid) -> Result<bool> {
192    let tx = conn.unchecked_transaction()?;
193
194    // execute returns how many rows were affected.
195    let exists = tx.execute(
196        "DELETE FROM credit_cards_data
197        WHERE guid = :guid",
198        rusqlite::named_params! {
199            ":guid": guid.as_str(),
200        },
201    )? != 0;
202
203    tx.commit()?;
204    Ok(exists)
205}
206
207pub fn scrub_encrypted_credit_card_data(conn: &Connection) -> Result<()> {
208    let tx = conn.unchecked_transaction()?;
209    tx.execute("UPDATE credit_cards_data SET cc_number_enc = ''", [])?;
210    tx.commit()?;
211    Ok(())
212}
213
214pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> {
215    let tx = conn.unchecked_transaction()?;
216    let now_ms = Timestamp::now();
217
218    tx.execute(
219        "UPDATE credit_cards_data
220        SET time_last_used              = :time_last_used,
221            times_used                  = times_used + 1,
222            sync_change_counter         = sync_change_counter + 1
223        WHERE guid                      = :guid",
224        rusqlite::named_params! {
225            ":time_last_used": now_ms,
226            ":guid": guid.as_str(),
227        },
228    )?;
229
230    tx.commit()?;
231    Ok(())
232}
233
234#[cfg(test)]
235pub(crate) mod tests {
236    use super::*;
237    use crate::db::test::new_mem_db;
238    use crate::encryption::EncryptorDecryptor;
239    use nss::ensure_initialized;
240    use sync15::bso::IncomingBso;
241
242    pub fn get_all(
243        conn: &Connection,
244        table_name: String,
245    ) -> rusqlite::Result<Vec<String>, rusqlite::Error> {
246        let mut stmt = conn.prepare(&format!(
247            "SELECT guid FROM {table_name}",
248            table_name = table_name
249        ))?;
250        let rows = stmt.query_map([], |row| row.get(0))?;
251
252        let mut guids = Vec::new();
253        for guid_result in rows {
254            guids.push(guid_result?);
255        }
256
257        Ok(guids)
258    }
259
260    pub fn insert_tombstone_record(
261        conn: &Connection,
262        guid: String,
263    ) -> rusqlite::Result<usize, rusqlite::Error> {
264        conn.execute(
265            "INSERT INTO credit_cards_tombstones (
266                guid,
267                time_deleted
268            ) VALUES (
269                :guid,
270                :time_deleted
271            )",
272            rusqlite::named_params! {
273                ":guid": guid,
274                ":time_deleted": Timestamp::now(),
275            },
276        )
277    }
278
279    pub(crate) fn test_insert_mirror_record(conn: &Connection, bso: IncomingBso) {
280        // This test function is a bit suspect, because credit-cards always
281        // store encrypted records, which this ignores entirely, and stores the
282        // raw payload with a cleartext cc_number.
283        // It's OK for all current test consumers, but it's a bit of a smell...
284        conn.execute(
285            "INSERT INTO credit_cards_mirror (guid, payload)
286             VALUES (:guid, :payload)",
287            rusqlite::named_params! {
288                ":guid": &bso.envelope.id,
289                ":payload": &bso.payload,
290            },
291        )
292        .expect("should insert");
293    }
294
295    #[test]
296    fn test_credit_card_create_and_read() -> Result<()> {
297        ensure_initialized();
298        let db = new_mem_db();
299
300        let saved_credit_card = add_credit_card(
301            &db,
302            UpdatableCreditCardFields {
303                cc_name: "jane doe".to_string(),
304                cc_number_enc: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
305                cc_number_last_4: "1234".to_string(),
306                cc_exp_month: 3,
307                cc_exp_year: 2022,
308                cc_type: "visa".to_string(),
309            },
310        )?;
311
312        // check that the add function populated the guid field
313        assert_ne!(Guid::default(), saved_credit_card.guid);
314
315        // check that the time created and time last modified were set
316        assert_ne!(0, saved_credit_card.metadata.time_created.as_millis());
317        assert_ne!(0, saved_credit_card.metadata.time_last_modified.as_millis());
318
319        // check that sync_change_counter was set to 0.
320        assert_eq!(0, saved_credit_card.metadata.sync_change_counter);
321
322        // get created credit card
323        let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
324
325        assert_eq!(saved_credit_card.guid, retrieved_credit_card.guid);
326        assert_eq!(saved_credit_card.cc_name, retrieved_credit_card.cc_name);
327        assert_eq!(
328            saved_credit_card.cc_number_enc,
329            retrieved_credit_card.cc_number_enc
330        );
331        assert_eq!(
332            saved_credit_card.cc_number_last_4,
333            retrieved_credit_card.cc_number_last_4
334        );
335        assert_eq!(
336            saved_credit_card.cc_exp_month,
337            retrieved_credit_card.cc_exp_month
338        );
339        assert_eq!(
340            saved_credit_card.cc_exp_year,
341            retrieved_credit_card.cc_exp_year
342        );
343        assert_eq!(saved_credit_card.cc_type, retrieved_credit_card.cc_type);
344
345        // converting the created record into a tombstone to check that it's not returned on a second `get_credit_card` call
346        let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
347        assert!(delete_result.is_ok());
348        assert!(delete_result?);
349
350        assert!(get_credit_card(&db, &saved_credit_card.guid).is_err());
351
352        Ok(())
353    }
354
355    #[test]
356    fn test_credit_card_missing_guid() {
357        ensure_initialized();
358        let db = new_mem_db();
359        let guid = Guid::random();
360        let result = get_credit_card(&db, &guid);
361
362        assert_eq!(
363            result.unwrap_err().to_string(),
364            Error::NoSuchRecord(guid.to_string()).to_string()
365        );
366    }
367
368    #[test]
369    fn test_credit_card_read_all() -> Result<()> {
370        ensure_initialized();
371        let db = new_mem_db();
372
373        let saved_credit_card = add_credit_card(
374            &db,
375            UpdatableCreditCardFields {
376                cc_name: "jane doe".to_string(),
377                cc_number_enc: "YYYYYYYYYYYYYYYYYYYYYYYYYYYYY".to_string(),
378                cc_number_last_4: "4321".to_string(),
379                cc_exp_month: 3,
380                cc_exp_year: 2022,
381                cc_type: "visa".to_string(),
382            },
383        )?;
384
385        let saved_credit_card2 = add_credit_card(
386            &db,
387            UpdatableCreditCardFields {
388                cc_name: "john deer".to_string(),
389                cc_number_enc: "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
390                cc_number_last_4: "6543".to_string(),
391                cc_exp_month: 10,
392                cc_exp_year: 2025,
393                cc_type: "mastercard".to_string(),
394            },
395        )?;
396
397        // creating a third credit card with a tombstone to ensure it's not returned
398        let saved_credit_card3 = add_credit_card(
399            &db,
400            UpdatableCreditCardFields {
401                cc_name: "abraham lincoln".to_string(),
402                cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
403                cc_number_last_4: "9876".to_string(),
404                cc_exp_month: 1,
405                cc_exp_year: 2024,
406                cc_type: "amex".to_string(),
407            },
408        )?;
409
410        let delete_result = delete_credit_card(&db, &saved_credit_card3.guid);
411        assert!(delete_result.is_ok());
412        assert!(delete_result?);
413
414        let retrieved_credit_cards = get_all_credit_cards(&db)?;
415
416        assert!(!retrieved_credit_cards.is_empty());
417        let expected_number_of_credit_cards = 2;
418        assert_eq!(
419            expected_number_of_credit_cards,
420            retrieved_credit_cards.len()
421        );
422
423        let retrieved_credit_card_guids = [
424            retrieved_credit_cards[0].guid.as_str(),
425            retrieved_credit_cards[1].guid.as_str(),
426        ];
427        assert!(retrieved_credit_card_guids.contains(&saved_credit_card.guid.as_str()));
428        assert!(retrieved_credit_card_guids.contains(&saved_credit_card2.guid.as_str()));
429
430        Ok(())
431    }
432
433    #[test]
434    fn test_credit_card_update() -> Result<()> {
435        ensure_initialized();
436        let db = new_mem_db();
437
438        let saved_credit_card = add_credit_card(
439            &db,
440            UpdatableCreditCardFields {
441                cc_name: "john deer".to_string(),
442                cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
443                cc_number_last_4: "4321".to_string(),
444                cc_exp_month: 10,
445                cc_exp_year: 2025,
446                cc_type: "mastercard".to_string(),
447            },
448        )?;
449
450        let expected_cc_name = "john doe".to_string();
451        let update_result = update_credit_card(
452            &db,
453            &saved_credit_card.guid,
454            &UpdatableCreditCardFields {
455                cc_name: expected_cc_name.clone(),
456                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
457                cc_number_last_4: "1234".to_string(),
458                cc_type: "mastercard".to_string(),
459                cc_exp_month: 10,
460                cc_exp_year: 2025,
461            },
462        );
463        assert!(update_result.is_ok());
464
465        let updated_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
466
467        assert_eq!(saved_credit_card.guid, updated_credit_card.guid);
468        assert_eq!(expected_cc_name, updated_credit_card.cc_name);
469
470        //check that the sync_change_counter was incremented
471        assert_eq!(1, updated_credit_card.metadata.sync_change_counter);
472
473        Ok(())
474    }
475
476    #[test]
477    fn test_credit_card_update_internal_credit_card() -> Result<()> {
478        ensure_initialized();
479        let mut db = new_mem_db();
480        let tx = db.transaction()?;
481
482        let guid = Guid::random();
483        add_internal_credit_card(
484            &tx,
485            &InternalCreditCard {
486                guid: guid.clone(),
487                cc_name: "john deer".to_string(),
488                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
489                cc_number_last_4: "1234".to_string(),
490                cc_exp_month: 10,
491                cc_exp_year: 2025,
492                cc_type: "mastercard".to_string(),
493                ..Default::default()
494            },
495        )?;
496
497        let expected_cc_exp_month = 11;
498        update_internal_credit_card(
499            &tx,
500            &InternalCreditCard {
501                guid: guid.clone(),
502                cc_name: "john deer".to_string(),
503                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
504                cc_number_last_4: "1234".to_string(),
505                cc_exp_month: expected_cc_exp_month,
506                cc_exp_year: 2025,
507                cc_type: "mastercard".to_string(),
508                ..Default::default()
509            },
510            false,
511        )?;
512
513        let record_exists: bool = tx.query_row(
514            "SELECT EXISTS (
515                SELECT 1
516                FROM credit_cards_data
517                WHERE guid = :guid
518                AND cc_exp_month = :cc_exp_month
519                AND sync_change_counter = 0
520            )",
521            [&guid.to_string(), &expected_cc_exp_month.to_string()],
522            |row| row.get(0),
523        )?;
524        assert!(record_exists);
525
526        Ok(())
527    }
528
529    #[test]
530    fn test_credit_card_delete() -> Result<()> {
531        ensure_initialized();
532        let db = new_mem_db();
533        let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
534
535        let saved_credit_card = add_credit_card(
536            &db,
537            UpdatableCreditCardFields {
538                cc_name: "john deer".to_string(),
539                cc_number_enc: encdec.encrypt("1234567812345678")?,
540                cc_number_last_4: "5678".to_string(),
541                cc_exp_month: 10,
542                cc_exp_year: 2025,
543                cc_type: "mastercard".to_string(),
544            },
545        )?;
546
547        let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
548        assert!(delete_result.is_ok());
549        assert!(delete_result?);
550
551        let saved_credit_card2 = add_credit_card(
552            &db,
553            UpdatableCreditCardFields {
554                cc_name: "john doe".to_string(),
555                cc_number_enc: encdec.encrypt("1234123412341234")?,
556                cc_number_last_4: "1234".to_string(),
557                cc_exp_month: 5,
558                cc_exp_year: 2024,
559                cc_type: "visa".to_string(),
560            },
561        )?;
562
563        // create a mirror record to check that a tombstone record is created upon deletion
564        let cc2_guid = saved_credit_card2.guid.clone();
565        let payload = saved_credit_card2.into_test_incoming_bso(&encdec, Default::default());
566
567        test_insert_mirror_record(&db, payload);
568
569        let delete_result2 = delete_credit_card(&db, &cc2_guid);
570        assert!(delete_result2.is_ok());
571        assert!(delete_result2?);
572
573        // check that a tombstone record exists since the record existed in the mirror
574        let tombstone_exists: bool = db.query_row(
575            "SELECT EXISTS (
576                SELECT 1
577                FROM credit_cards_tombstones
578                WHERE guid = :guid
579            )",
580            [&cc2_guid],
581            |row| row.get(0),
582        )?;
583        assert!(tombstone_exists);
584
585        // remove the tombstone record
586        db.execute(
587            "DELETE FROM credit_cards_tombstones
588            WHERE guid = :guid",
589            rusqlite::named_params! {
590                ":guid": cc2_guid,
591            },
592        )?;
593
594        Ok(())
595    }
596
597    #[test]
598    fn test_scrub_encrypted_credit_card_data() -> Result<()> {
599        ensure_initialized();
600        let db = new_mem_db();
601        let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
602        let mut saved_credit_cards = Vec::with_capacity(10);
603        for _ in 0..5 {
604            saved_credit_cards.push(add_credit_card(
605                &db,
606                UpdatableCreditCardFields {
607                    cc_name: "john deer".to_string(),
608                    cc_number_enc: encdec.encrypt("1234567812345678")?,
609                    cc_number_last_4: "5678".to_string(),
610                    cc_exp_month: 10,
611                    cc_exp_year: 2025,
612                    cc_type: "mastercard".to_string(),
613                },
614            )?);
615        }
616
617        scrub_encrypted_credit_card_data(&db)?;
618        for saved_credit_card in saved_credit_cards.into_iter() {
619            let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
620            assert_eq!(retrieved_credit_card.cc_number_enc, "");
621        }
622
623        Ok(())
624    }
625
626    #[test]
627    fn test_credit_card_trigger_on_create() -> Result<()> {
628        ensure_initialized();
629        let db = new_mem_db();
630        let tx = db.unchecked_transaction()?;
631        let guid = Guid::random();
632
633        // create a tombstone record
634        insert_tombstone_record(&db, guid.to_string())?;
635
636        // create a new credit card with the tombstone's guid
637        let credit_card = InternalCreditCard {
638            guid,
639            cc_name: "john deer".to_string(),
640            cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
641            cc_number_last_4: "6543".to_string(),
642            cc_exp_month: 10,
643            cc_exp_year: 2025,
644            cc_type: "mastercard".to_string(),
645
646            ..Default::default()
647        };
648
649        let add_credit_card_result = add_internal_credit_card(&tx, &credit_card);
650        assert!(add_credit_card_result.is_err());
651
652        let expected_error_message = "guid exists in `credit_cards_tombstones`";
653        assert!(add_credit_card_result
654            .unwrap_err()
655            .to_string()
656            .contains(expected_error_message));
657
658        Ok(())
659    }
660
661    #[test]
662    fn test_credit_card_trigger_on_delete() -> Result<()> {
663        ensure_initialized();
664        let db = new_mem_db();
665        let tx = db.unchecked_transaction()?;
666        let guid = Guid::random();
667
668        // create an credit card
669        let credit_card = InternalCreditCard {
670            guid,
671            cc_name: "jane doe".to_string(),
672            cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
673            cc_number_last_4: "6543".to_string(),
674            cc_exp_month: 3,
675            cc_exp_year: 2022,
676            cc_type: "visa".to_string(),
677            ..Default::default()
678        };
679        add_internal_credit_card(&tx, &credit_card)?;
680
681        // create a tombstone record with the same guid
682        let tombstone_result = insert_tombstone_record(&db, credit_card.guid.to_string());
683
684        let expected_error_message = "guid exists in `credit_cards_data`";
685        assert!(tombstone_result
686            .unwrap_err()
687            .to_string()
688            .contains(expected_error_message));
689
690        Ok(())
691    }
692
693    #[test]
694    fn test_credit_card_touch() -> Result<()> {
695        ensure_initialized();
696        let db = new_mem_db();
697        let saved_credit_card = add_credit_card(
698            &db,
699            UpdatableCreditCardFields {
700                cc_name: "john doe".to_string(),
701                cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
702                cc_number_last_4: "6543".to_string(),
703                cc_exp_month: 5,
704                cc_exp_year: 2024,
705                cc_type: "visa".to_string(),
706            },
707        )?;
708
709        assert_eq!(saved_credit_card.metadata.sync_change_counter, 0);
710        assert_eq!(saved_credit_card.metadata.times_used, 0);
711
712        touch(&db, &saved_credit_card.guid)?;
713
714        let touched_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
715
716        assert_eq!(touched_credit_card.metadata.sync_change_counter, 1);
717        assert_eq!(touched_credit_card.metadata.times_used, 1);
718
719        Ok(())
720    }
721}