Bug 1359978 - (Part 2) Add credit card support to ProfileStorage.jsm. r=MattN,steveck
authorLuke Chang <lchang@mozilla.com>
Fri, 28 Apr 2017 18:19:53 -0700
changeset 358367 e6f6f7fa042669be3a45d668e9eb93b7aefce45c
parent 358366 376cb53491aacebd54427e323fa75a1a141c8919
child 358368 1d809d859e034204e2d47d5356307831ec862f0a
push id42661
push userlchang@mozilla.com
push dateMon, 15 May 2017 02:28:20 +0000
treeherderautoland@e6f6f7fa0426 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, steveck
bugs1359978
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1359978 - (Part 2) Add credit card support to ProfileStorage.jsm. r=MattN,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]