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