Bug 1359978 - (Part 2) Add credit card support to ProfileStorage.jsm. r=MattN, r=steveck draft
authorLuke Chang <lchang@mozilla.com>
Fri, 28 Apr 2017 18:19:53 -0700
changeset 577587 20a9cd010e448275dd1fd848c669224b08dfcdc5
parent 577586 f4aeb206de537f6b6b532b09fe2b5ee6599890d9
child 628533 c11854f38d815390271adc2241a8902120a0372e
push id58724
push userbmo:lchang@mozilla.com
push dateMon, 15 May 2017 02:25:44 +0000
reviewersMattN, steveck
bugs1359978
milestone55.0a1
Bug 1359978 - (Part 2) Add credit card support to ProfileStorage.jsm. r=MattN, r=steveck MozReview-Commit-ID: JzojiHGcomp
browser/extensions/formautofill/ProfileStorage.jsm
browser/extensions/formautofill/test/unit/test_creditCardRecords.js
browser/extensions/formautofill/test/unit/test_transformFields.js
browser/extensions/formautofill/test/unit/xpcshell.ini
--- a/browser/extensions/formautofill/ProfileStorage.jsm
+++ b/browser/extensions/formautofill/ProfileStorage.jsm
@@ -36,16 +36,41 @@
  *       address-line3,
  *
  *       // metadata
  *       timeCreated,          // in ms
  *       timeLastUsed,         // in ms
  *       timeLastModified,     // in ms
  *       timesUsed
  *     }
+ *   ],
+ *   creditCards: [
+ *     {
+ *       guid,                 // 12 characters
+ *
+ *       // credit card fields
+ *       cc-name,
+ *       cc-number-encrypted,
+ *       cc-number-masked,     // e.g. ************1234
+ *       cc-exp-month,
+ *       cc-exp-year,          // 2-digit year will be converted to 4 digits
+ *                             // upon saving
+ *
+ *       // computed fields (These fields are not stored in the file as they are
+ *       // generated at runtime.)
+ *       cc-given-name,
+ *       cc-additional-name,
+ *       cc-family-name,
+ *
+ *       // metadata
+ *       timeCreated,          // in ms
+ *       timeLastUsed,         // in ms
+ *       timeLastModified,     // in ms
+ *       timesUsed
+ *     }
  *   ]
  * }
  */
 
 "use strict";
 
 // We expose a singleton from this module. Some tests may import the
 // constructor via a backstage pass.
@@ -81,30 +106,38 @@ const VALID_PROFILE_FIELDS = [
   "address-level2",
   "address-level1",
   "postal-code",
   "country",
   "tel",
   "email",
 ];
 
+const VALID_CREDIT_CARD_FIELDS = [
+  "cc-name",
+  "cc-number-encrypted",
+  "cc-number-masked",
+  "cc-exp-month",
+  "cc-exp-year",
+];
+
 const INTERNAL_FIELDS = [
   "guid",
   "timeCreated",
   "timeLastUsed",
   "timeLastModified",
   "timesUsed",
 ];
 
 /**
  * Class that manipulates records in a specified collection.
  *
  * Note that it is responsible for converting incoming data to a consistent
  * format in the storage. For example, computed fields will be transformed to
- * the original fields.
+ * the original fields and 2-digit years will be calculated into 4 digits.
  */
 class AutofillRecords {
   /**
    * Creates an AutofillRecords.
    *
    * @param {JSONFile} store
    *        An instance of JSONFile.
    * @param {string} collectionName
@@ -406,31 +439,126 @@ class Addresses extends AutofillRecords 
       // Concatenate "address-line*" if "street-address" is omitted.
       if (!profile["street-address"]) {
         profile["street-address"] = addressLines.join("\n");
       }
     }
   }
 }
 
+class CreditCards extends AutofillRecords {
+  constructor(store) {
+    super(store, "creditCards", VALID_CREDIT_CARD_FIELDS);
+  }
+
+  _recordReadProcessor(creditCard, {noComputedFields} = {}) {
+    if (noComputedFields) {
+      return;
+    }
+
+    // Compute split names
+    if (creditCard["cc-name"]) {
+      let nameParts = FormAutofillNameUtils.splitName(creditCard["cc-name"]);
+      if (nameParts.given) {
+        creditCard["cc-given-name"] = nameParts.given;
+      }
+      if (nameParts.middle) {
+        creditCard["cc-additional-name"] = nameParts.middle;
+      }
+      if (nameParts.family) {
+        creditCard["cc-family-name"] = nameParts.family;
+      }
+    }
+  }
+
+  _recordWriteProcessor(creditCard) {
+    // Fields that should not be set by content.
+    delete creditCard["cc-number-encrypted"];
+    delete creditCard["cc-number-masked"];
+
+    // Validate and encrypt credit card numbers, and calculate the masked numbers
+    if (creditCard["cc-number"]) {
+      let ccNumber = creditCard["cc-number"].replace(/\s/g, "");
+      delete creditCard["cc-number"];
+
+      if (!/^\d+$/.test(ccNumber)) {
+        throw new Error("Credit card number contains invalid characters.");
+      }
+
+      // TODO: Encrypt cc-number here (bug 1337314).
+      // e.g. creditCard["cc-number-encrypted"] = Encrypt(creditCard["cc-number"]);
+
+      if (ccNumber.length > 4) {
+        creditCard["cc-number-masked"] = "*".repeat(ccNumber.length - 4) + ccNumber.substr(-4);
+      } else {
+        creditCard["cc-number-masked"] = ccNumber;
+      }
+    }
+
+    // Normalize name
+    if (creditCard["cc-given-name"] || creditCard["cc-additional-name"] || creditCard["cc-family-name"]) {
+      if (!creditCard["cc-name"]) {
+        creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({
+          given: creditCard["cc-given-name"],
+          middle: creditCard["cc-additional-name"],
+          family: creditCard["cc-family-name"],
+        });
+      }
+
+      delete creditCard["cc-given-name"];
+      delete creditCard["cc-additional-name"];
+      delete creditCard["cc-family-name"];
+    }
+
+    // Validate expiry date
+    if (creditCard["cc-exp-month"]) {
+      let expMonth = parseInt(creditCard["cc-exp-month"], 10);
+      if (isNaN(expMonth) || expMonth < 1 || expMonth > 12) {
+        delete creditCard["cc-exp-month"];
+      } else {
+        creditCard["cc-exp-month"] = expMonth;
+      }
+    }
+    if (creditCard["cc-exp-year"]) {
+      let expYear = parseInt(creditCard["cc-exp-year"], 10);
+      if (isNaN(expYear) || expYear < 0) {
+        delete creditCard["cc-exp-year"];
+      } else if (expYear < 100) {
+        // Enforce 4 digits years.
+        creditCard["cc-exp-year"] = expYear + 2000;
+      } else {
+        creditCard["cc-exp-year"] = expYear;
+      }
+    }
+  }
+}
+
 function ProfileStorage(path) {
   this._path = path;
   this._initializePromise = null;
   this.INTERNAL_FIELDS = INTERNAL_FIELDS;
 }
 
 ProfileStorage.prototype = {
   get addresses() {
     if (!this._addresses) {
       this._store.ensureDataReady();
       this._addresses = new Addresses(this._store);
     }
     return this._addresses;
   },
 
+  get creditCards() {
+    if (!this._creditCards) {
+      this._store.ensureDataReady();
+      this._creditCards = new CreditCards(this._store);
+    }
+    return this._creditCards;
+  },
+
   /**
    * Loads the profile data from file to memory.
    *
    * @returns {Promise}
    * @resolves When the operation finished successfully.
    * @rejects  JavaScript exception.
    */
   initialize() {
@@ -444,16 +572,19 @@ ProfileStorage.prototype = {
     return this._initializePromise;
   },
 
   _dataPostProcessor(data) {
     data.version = SCHEMA_VERSION;
     if (!data.addresses) {
       data.addresses = [];
     }
+    if (!data.creditCards) {
+      data.creditCards = [];
+    }
     return data;
   },
 
   // For test only.
   _saveImmediately() {
     return this._store._save();
   },
 };
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -0,0 +1,325 @@
+/**
+ * Tests ProfileStorage object with creditCards records.
+ */
+
+"use strict";
+
+const {ProfileStorage} = Cu.import("resource://formautofill/ProfileStorage.jsm", {});
+
+const TEST_STORE_FILE_NAME = "test-credit-card.json";
+
+const TEST_CREDIT_CARD_1 = {
+  "cc-name": "John Doe",
+  "cc-number": "1234567812345678",
+  "cc-exp-month": 4,
+  "cc-exp-year": 2017,
+};
+
+const TEST_CREDIT_CARD_2 = {
+  "cc-name": "Timothy Berners-Lee",
+  "cc-number": "1111222233334444",
+  "cc-exp-month": 12,
+  "cc-exp-year": 2022,
+};
+
+const TEST_CREDIT_CARD_3 = {
+  "cc-number": "9999888877776666",
+  "cc-exp-month": 1,
+  "cc-exp-year": 2000,
+};
+
+const TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR = {
+  "cc-number": "1234123412341234",
+  "cc-exp-month": 1,
+  "cc-exp-year": 12,
+};
+
+const TEST_CREDIT_CARD_WITH_INVALID_FIELD = {
+  "cc-name": "John Doe",
+  invalidField: "INVALID",
+};
+
+const TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE = {
+  "cc-name": "John Doe",
+  "cc-number": "1111222233334444",
+  "cc-exp-month": 13,
+  "cc-exp-year": -3,
+};
+
+const TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS = {
+  "cc-name": "John Doe",
+  "cc-number": "1111 2222 3333 4444",
+};
+
+const TEST_CREDIT_CARD_WITH_INVALID_NUMBERS = {
+  "cc-name": "John Doe",
+  "cc-number": "abcdefg",
+};
+
+let prepareTestCreditCards = async function(path) {
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                          (subject, data) => data == "add");
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_1);
+  await onChanged;
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_2);
+  await profileStorage._saveImmediately();
+};
+
+let reCCNumber = /^(\*+)(.{4})$/;
+
+let do_check_credit_card_matches = (creditCardWithMeta, creditCard) => {
+  for (let key in creditCard) {
+    if (key == "cc-number") {
+      do_check_eq(creditCardWithMeta["cc-number"], undefined);
+
+      // check "cc-number-encrypted" after encryption lands (bug 1337314).
+
+      let matches = reCCNumber.exec(creditCardWithMeta["cc-number-masked"]);
+      do_check_neq(matches, null);
+      do_check_eq(creditCardWithMeta["cc-number-masked"].length, creditCard["cc-number"].length);
+      do_check_eq(creditCard["cc-number"].endsWith(matches[2]), true);
+    } else {
+      do_check_eq(creditCardWithMeta[key], creditCard[key]);
+    }
+  }
+};
+
+add_task(async function test_initialize() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  do_check_eq(profileStorage._store.data.version, 1);
+  do_check_eq(profileStorage._store.data.creditCards.length, 0);
+
+  let data = profileStorage._store.data;
+  Assert.deepEqual(data.creditCards, []);
+
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  Assert.deepEqual(profileStorage._store.data, data);
+});
+
+add_task(async function test_getAll() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+
+  do_check_eq(creditCards.length, 2);
+  do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
+  do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
+
+  // Check computed fields.
+  do_check_eq(creditCards[0]["cc-given-name"], "John");
+  do_check_eq(creditCards[0]["cc-family-name"], "Doe");
+
+  // Test with noComputedFields set.
+  creditCards = profileStorage.creditCards.getAll({noComputedFields: true});
+  do_check_eq(creditCards[0]["cc-given-name"], undefined);
+  do_check_eq(creditCards[0]["cc-family-name"], undefined);
+
+  // Modifying output shouldn't affect the storage.
+  creditCards[0]["cc-name"] = "test";
+  do_check_credit_card_matches(profileStorage.creditCards.getAll()[0], TEST_CREDIT_CARD_1);
+});
+
+add_task(async function test_get() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+  let guid = creditCards[0].guid;
+
+  let creditCard = profileStorage.creditCards.get(guid);
+  do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_1);
+
+  // Modifying output shouldn't affect the storage.
+  creditCards[0]["cc-name"] = "test";
+  do_check_credit_card_matches(profileStorage.creditCards.get(guid), TEST_CREDIT_CARD_1);
+
+  Assert.throws(() => profileStorage.creditCards.get("INVALID_GUID"),
+    /No matching record\./);
+});
+
+add_task(async function test_getByFilter() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let filter = {info: {fieldName: "cc-name"}, searchString: "Tim"};
+  let creditCards = profileStorage.creditCards.getByFilter(filter);
+  do_check_eq(creditCards.length, 1);
+  do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_2);
+
+  // TODO: Uncomment this after decryption lands (bug 1337314).
+  // filter = {info: {fieldName: "cc-number"}, searchString: "11"};
+  // creditCards = profileStorage.creditCards.getByFilter(filter);
+  // do_check_eq(creditCards.length, 1);
+  // do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_2);
+});
+
+add_task(async function test_add() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+
+  do_check_eq(creditCards.length, 2);
+
+  do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
+  do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
+
+  do_check_neq(creditCards[0].guid, undefined);
+  do_check_neq(creditCards[0].timeCreated, undefined);
+  do_check_eq(creditCards[0].timeLastModified, creditCards[0].timeCreated);
+  do_check_eq(creditCards[0].timeLastUsed, 0);
+  do_check_eq(creditCards[0].timesUsed, 0);
+
+  Assert.throws(() => profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_FIELD),
+    /"invalidField" is not a valid field\./);
+});
+
+add_task(async function test_update() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+  let guid = creditCards[1].guid;
+  let timeLastModified = creditCards[1].timeLastModified;
+
+  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                          (subject, data) => data == "update");
+
+  do_check_neq(creditCards[1]["cc-name"], undefined);
+
+  profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_3);
+  await onChanged;
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCard = profileStorage.creditCards.get(guid);
+
+  do_check_eq(creditCard["cc-name"], undefined);
+  do_check_neq(creditCard.timeLastModified, timeLastModified);
+  do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_3);
+
+  Assert.throws(
+    () => profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
+    /No matching record\./
+  );
+
+  Assert.throws(
+    () => profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_WITH_INVALID_FIELD),
+    /"invalidField" is not a valid field\./
+  );
+});
+
+add_task(async function test_validate() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE);
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR);
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS);
+
+  let creditCards = profileStorage.creditCards.getAll();
+
+  do_check_eq(creditCards[0]["cc-exp-month"], undefined);
+  do_check_eq(creditCards[0]["cc-exp-year"], undefined);
+
+  do_check_eq(creditCards[1]["cc-exp-month"], TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-month"]);
+  do_check_eq(creditCards[1]["cc-exp-year"],
+    parseInt(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-year"], 10) + 2000);
+
+  do_check_eq(creditCards[2]["cc-number-masked"].length, 16);
+  // TODO: Check the decrypted numbers should not contain spaces after
+  //       decryption lands (bug 1337314).
+
+  Assert.throws(() => profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_NUMBERS),
+    /Credit card number contains invalid characters\./);
+});
+
+add_task(async function test_notifyUsed() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+  let guid = creditCards[1].guid;
+  let timeLastUsed = creditCards[1].timeLastUsed;
+  let timesUsed = creditCards[1].timesUsed;
+
+  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                          (subject, data) => data == "notifyUsed");
+
+  profileStorage.creditCards.notifyUsed(guid);
+  await onChanged;
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCard = profileStorage.creditCards.get(guid);
+
+  do_check_eq(creditCard.timesUsed, timesUsed + 1);
+  do_check_neq(creditCard.timeLastUsed, timeLastUsed);
+
+  Assert.throws(() => profileStorage.creditCards.notifyUsed("INVALID_GUID"),
+    /No matching record\./);
+});
+
+add_task(async function test_remove() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+  await prepareTestCreditCards(path);
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+  let guid = creditCards[1].guid;
+
+  let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+                                          (subject, data) => data == "remove");
+
+  do_check_eq(creditCards.length, 2);
+
+  profileStorage.creditCards.remove(guid);
+  await onChanged;
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  creditCards = profileStorage.creditCards.getAll();
+
+  do_check_eq(creditCards.length, 1);
+
+  Assert.throws(() => profileStorage.creditCards.get(guid), /No matching record\./);
+});
--- a/browser/extensions/formautofill/test/unit/test_transformFields.js
+++ b/browser/extensions/formautofill/test/unit/test_transformFields.js
@@ -175,16 +175,75 @@ const ADDRESS_NORMALIZE_TESTCASES = [
       "address-line3": "line3",
     },
     expectedResult: {
       "street-address": "street address\nstreet address line 2",
     },
   },
 ];
 
+const CREDIT_CARD_COMPUTE_TESTCASES = [
+  // Empty
+  {
+    description: "Empty credit card",
+    creditCard: {
+    },
+    expectedResult: {
+    },
+  },
+
+  // Name
+  {
+    description: "Has \"cc-name\"",
+    creditCard: {
+      "cc-name": "Timothy John Berners-Lee",
+    },
+    expectedResult: {
+      "cc-name": "Timothy John Berners-Lee",
+      "cc-given-name": "Timothy",
+      "cc-additional-name": "John",
+      "cc-family-name": "Berners-Lee",
+    },
+  },
+];
+
+const CREDIT_CARD_NORMALIZE_TESTCASES = [
+  // Empty
+  {
+    description: "Empty credit card",
+    creditCard: {
+    },
+    expectedResult: {
+    },
+  },
+
+  // Name
+  {
+    description: "Has both \"cc-name\" and the split name fields",
+    creditCard: {
+      "cc-name": "Timothy John Berners-Lee",
+      "cc-given-name": "John",
+      "cc-family-name": "Doe",
+    },
+    expectedResult: {
+      "cc-name": "Timothy John Berners-Lee",
+    },
+  },
+  {
+    description: "Has only the split name fields",
+    creditCard: {
+      "cc-given-name": "John",
+      "cc-family-name": "Doe",
+    },
+    expectedResult: {
+      "cc-name": "John Doe",
+    },
+  },
+];
+
 let do_check_record_matches = (expectedRecord, record) => {
   for (let key in expectedRecord) {
     do_check_eq(expectedRecord[key], record[key] || "");
   }
 };
 
 add_task(async function test_computeAddressFields() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
@@ -220,8 +279,48 @@ add_task(async function test_normalizeAd
 
   let addresses = profileStorage.addresses.getAll();
 
   for (let i in addresses) {
     do_print("Verify testcase: " + ADDRESS_NORMALIZE_TESTCASES[i].description);
     do_check_record_matches(ADDRESS_NORMALIZE_TESTCASES[i].expectedResult, addresses[i]);
   }
 });
+
+add_task(async function test_computeCreditCardFields() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  CREDIT_CARD_COMPUTE_TESTCASES.forEach(testcase => profileStorage.creditCards.add(testcase.creditCard));
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+
+  for (let i in creditCards) {
+    do_print("Verify testcase: " + CREDIT_CARD_COMPUTE_TESTCASES[i].description);
+    do_check_record_matches(CREDIT_CARD_COMPUTE_TESTCASES[i].expectedResult, creditCards[i]);
+  }
+});
+
+add_task(async function test_normalizeCreditCardFields() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  CREDIT_CARD_NORMALIZE_TESTCASES.forEach(testcase => profileStorage.creditCards.add(testcase.creditCard));
+  await profileStorage._saveImmediately();
+
+  profileStorage = new ProfileStorage(path);
+  await profileStorage.initialize();
+
+  let creditCards = profileStorage.creditCards.getAll();
+
+  for (let i in creditCards) {
+    do_print("Verify testcase: " + CREDIT_CARD_NORMALIZE_TESTCASES[i].description);
+    do_check_record_matches(CREDIT_CARD_NORMALIZE_TESTCASES[i].expectedResult, creditCards[i]);
+  }
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -14,16 +14,17 @@ support-files =
 [heuristics/third_party/test_OfficeDepot.js]
 [heuristics/third_party/test_QVC.js]
 [heuristics/third_party/test_Sears.js]
 [heuristics/third_party/test_Staples.js]
 [heuristics/third_party/test_Walmart.js]
 [test_addressRecords.js]
 [test_autofillFormFields.js]
 [test_collectFormFields.js]
+[test_creditCardRecords.js]
 [test_enabledStatus.js]
 [test_findLabelElements.js]
 [test_getFormInputDetails.js]
 [test_isCJKName.js]
 [test_markAsAutofillField.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
 [test_profileAutocompleteResult.js]