author | Luke Chang <lchang@mozilla.com> |
Fri, 28 Apr 2017 18:19:53 -0700 | |
changeset 358367 | e6f6f7fa042669be3a45d668e9eb93b7aefce45c |
parent 358366 | 376cb53491aacebd54427e323fa75a1a141c8919 |
child 358368 | 1d809d859e034204e2d47d5356307831ec862f0a |
push id | 42661 |
push user | lchang@mozilla.com |
push date | Mon, 15 May 2017 02:28:20 +0000 |
treeherder | autoland@e6f6f7fa0426 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | MattN, steveck |
bugs | 1359978 |
milestone | 55.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
|
--- 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]