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