2 * Tests FormAutofillStorage object with creditCards records.
7 ChromeUtils.defineESModuleGetters(this, {
8 Preferences: "resource://gre/modules/Preferences.sys.mjs",
10 const { CreditCard } = ChromeUtils.importESModule(
11 "resource://gre/modules/CreditCard.sys.mjs"
14 let FormAutofillStorage;
15 let CREDIT_CARD_SCHEMA_VERSION;
16 add_setup(async () => {
17 ({ FormAutofillStorage } = ChromeUtils.importESModule(
18 "resource://autofill/FormAutofillStorage.sys.mjs"
20 ({ CREDIT_CARD_SCHEMA_VERSION } = ChromeUtils.importESModule(
21 "resource://autofill/FormAutofillStorageBase.sys.mjs"
25 const TEST_STORE_FILE_NAME = "test-credit-card.json";
26 const COLLECTION_NAME = "creditCards";
28 const TEST_CREDIT_CARD_1 = {
29 "cc-name": "John Doe",
30 "cc-number": "4929001587121045",
35 const TEST_CREDIT_CARD_2 = {
36 "cc-name": "Timothy Berners-Lee",
37 "cc-number": "5103059495477870",
42 const TEST_CREDIT_CARD_3 = {
43 "cc-number": "3589993783099582",
48 const TEST_CREDIT_CARD_WITH_BILLING_ADDRESS = {
49 "cc-name": "J. Smith",
50 "cc-number": "4111111111111111",
51 billingAddressGUID: "9m6hf4gfr6ge",
54 const TEST_CREDIT_CARD_WITH_EMPTY_FIELD = {
55 billingAddressGUID: "",
57 "cc-number": "344060747836806",
61 const TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD = {
63 "cc-additional-name": "",
66 "cc-number": "5415425865751454",
69 const TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR = {
70 "cc-number": "344060747836806",
75 const TEST_CREDIT_CARD_WITH_INVALID_FIELD = {
76 "cc-name": "John Doe",
77 "cc-number": "344060747836806",
78 "cc-type": { invalid: "invalid" },
81 const TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE = {
82 "cc-name": "John Doe",
83 "cc-number": "5103059495477870",
88 const TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS = {
89 "cc-name": "John Doe",
90 "cc-number": "5103 0594 9547 7870",
93 const TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE = {
97 const TEST_CREDIT_CARD_EMPTY_AFTER_UPDATE_CREDIT_CARD_1 = {
104 let prepareTestCreditCards = async function (path) {
105 let profileStorage = new FormAutofillStorage(path);
106 await profileStorage.initialize();
108 let onChanged = TestUtils.topicObserved(
109 "formautofill-storage-changed",
112 subject.wrappedJSObject.guid &&
113 subject.wrappedJSObject.collectionName == COLLECTION_NAME
115 Assert.ok(await profileStorage.creditCards.add(TEST_CREDIT_CARD_1));
117 Assert.ok(await profileStorage.creditCards.add(TEST_CREDIT_CARD_2));
119 await profileStorage._saveImmediately();
122 let reCCNumber = /^(\*+)(.{4})$/;
124 let do_check_credit_card_matches = (creditCardWithMeta, creditCard) => {
125 for (let key in creditCard) {
126 if (key == "cc-number") {
127 let matches = reCCNumber.exec(creditCardWithMeta["cc-number"]);
128 Assert.notEqual(matches, null);
130 creditCardWithMeta["cc-number"].length,
131 creditCard["cc-number"].length
133 Assert.equal(creditCard["cc-number"].endsWith(matches[2]), true);
134 Assert.notEqual(creditCard["cc-number-encrypted"], "");
136 Assert.equal(creditCardWithMeta[key], creditCard[key], "Testing " + key);
141 add_task(async function test_initialize() {
142 let path = getTempFile(TEST_STORE_FILE_NAME).path;
143 let profileStorage = new FormAutofillStorage(path);
144 await profileStorage.initialize();
146 Assert.equal(profileStorage._store.data.version, 1);
147 Assert.equal(profileStorage._store.data.creditCards.length, 0);
149 let data = profileStorage._store.data;
150 Assert.deepEqual(data.creditCards, []);
152 await profileStorage._saveImmediately();
154 profileStorage = new FormAutofillStorage(path);
155 await profileStorage.initialize();
157 Assert.deepEqual(profileStorage._store.data, data);
160 add_task(async function test_getAll() {
161 let path = getTempFile(TEST_STORE_FILE_NAME).path;
162 await prepareTestCreditCards(path);
164 let profileStorage = new FormAutofillStorage(path);
165 await profileStorage.initialize();
167 let creditCards = await profileStorage.creditCards.getAll();
169 Assert.equal(creditCards.length, 2);
170 do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
171 do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
173 // Check computed fields.
174 Assert.equal(creditCards[0]["cc-given-name"], "John");
175 Assert.equal(creditCards[0]["cc-family-name"], "Doe");
176 Assert.equal(creditCards[0]["cc-exp"], "2017-04");
178 // Test with rawData set.
179 creditCards = await profileStorage.creditCards.getAll({ rawData: true });
180 Assert.equal(creditCards[0]["cc-given-name"], undefined);
181 Assert.equal(creditCards[0]["cc-family-name"], undefined);
182 Assert.equal(creditCards[0]["cc-exp"], undefined);
184 // Modifying output shouldn't affect the storage.
185 creditCards[0]["cc-name"] = "test";
186 do_check_credit_card_matches(
187 (await profileStorage.creditCards.getAll())[0],
192 add_task(async function test_get() {
193 let path = getTempFile(TEST_STORE_FILE_NAME).path;
194 await prepareTestCreditCards(path);
196 let profileStorage = new FormAutofillStorage(path);
197 await profileStorage.initialize();
199 let creditCards = await profileStorage.creditCards.getAll();
200 let guid = creditCards[0].guid;
202 let creditCard = await profileStorage.creditCards.get(guid);
203 do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_1);
205 // Modifying output shouldn't affect the storage.
206 creditCards[0]["cc-name"] = "test";
207 do_check_credit_card_matches(
208 await profileStorage.creditCards.get(guid),
212 Assert.equal(await profileStorage.creditCards.get("INVALID_GUID"), null);
215 add_task(async function test_add() {
216 let path = getTempFile(TEST_STORE_FILE_NAME).path;
217 await prepareTestCreditCards(path);
219 let profileStorage = new FormAutofillStorage(path);
220 await profileStorage.initialize();
222 let creditCards = await profileStorage.creditCards.getAll();
224 Assert.equal(creditCards.length, 2);
226 do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
227 do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
229 Assert.notEqual(creditCards[0].guid, undefined);
230 Assert.equal(creditCards[0].version, CREDIT_CARD_SCHEMA_VERSION);
231 Assert.notEqual(creditCards[0].timeCreated, undefined);
232 Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
233 Assert.equal(creditCards[0].timeLastUsed, 0);
234 Assert.equal(creditCards[0].timesUsed, 0);
236 // Empty string should be deleted before saving.
237 await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
238 let creditCard = profileStorage.creditCards._data[2];
240 creditCard["cc-exp-month"],
241 TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"]
243 Assert.equal(creditCard["cc-name"], undefined);
244 Assert.equal(creditCard.billingAddressGUID, undefined);
246 // Empty computed fields shouldn't cause any problem.
247 await profileStorage.creditCards.add(
248 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD
250 creditCard = profileStorage.creditCards._data[3];
252 creditCard["cc-number"],
253 CreditCard.getLongMaskedNumber(
254 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]
258 await Assert.rejects(
259 profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_FIELD),
260 /"cc-type" contains invalid data type: object/
263 await Assert.rejects(
264 profileStorage.creditCards.add({}),
265 /Record contains no valid field\./
268 await Assert.rejects(
269 profileStorage.creditCards.add(TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE),
270 /Record contains no valid field\./
274 add_task(async function test_addWithBillingAddress() {
275 let path = getTempFile(TEST_STORE_FILE_NAME).path;
276 let profileStorage = new FormAutofillStorage(path);
277 await profileStorage.initialize();
279 let creditCards = await profileStorage.creditCards.getAll();
281 Assert.equal(creditCards.length, 0);
283 await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_BILLING_ADDRESS);
285 creditCards = await profileStorage.creditCards.getAll();
286 Assert.equal(creditCards.length, 1);
287 do_check_credit_card_matches(
289 TEST_CREDIT_CARD_WITH_BILLING_ADDRESS
293 add_task(async function test_update() {
294 // Test assumes that when an entry is saved a second time, it's last modified date will
295 // be different from the first. With high values of precision reduction, we execute too
296 // fast for that to be true.
297 let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
298 Preferences.set("privacy.reduceTimerPrecision", false);
300 registerCleanupFunction(function () {
301 Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
304 let path = getTempFile(TEST_STORE_FILE_NAME).path;
305 await prepareTestCreditCards(path);
307 let profileStorage = new FormAutofillStorage(path);
308 await profileStorage.initialize();
310 let creditCards = await profileStorage.creditCards.getAll();
311 let guid = creditCards[1].guid;
312 let timeLastModified = creditCards[1].timeLastModified;
314 let onChanged = TestUtils.topicObserved(
315 "formautofill-storage-changed",
318 subject.wrappedJSObject.guid == guid &&
319 subject.wrappedJSObject.collectionName == COLLECTION_NAME
322 Assert.notEqual(creditCards[1]["cc-name"], undefined);
323 await profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_3);
325 await profileStorage._saveImmediately();
327 profileStorage = new FormAutofillStorage(path);
328 await profileStorage.initialize();
330 let creditCard = await profileStorage.creditCards.get(guid);
332 Assert.equal(creditCard["cc-name"], undefined);
333 Assert.notEqual(creditCard.timeLastModified, timeLastModified);
334 do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_3);
336 // Empty string should be deleted while updating.
337 await profileStorage.creditCards.update(
338 profileStorage.creditCards._data[0].guid,
339 TEST_CREDIT_CARD_WITH_EMPTY_FIELD
341 creditCard = profileStorage.creditCards._data[0];
343 creditCard["cc-exp-month"],
344 TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"]
346 Assert.equal(creditCard["cc-name"], undefined);
347 Assert.equal(creditCard["cc-type"], "amex");
348 Assert.equal(creditCard.billingAddressGUID, undefined);
350 // Empty computed fields shouldn't cause any problem.
351 await profileStorage.creditCards.update(
352 profileStorage.creditCards._data[0].guid,
353 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD,
356 creditCard = profileStorage.creditCards._data[0];
358 creditCard["cc-number"],
359 CreditCard.getLongMaskedNumber(
360 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]
363 await profileStorage.creditCards.update(
364 profileStorage.creditCards._data[1].guid,
365 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD,
368 creditCard = profileStorage.creditCards._data[1];
370 creditCard["cc-number"],
371 CreditCard.getLongMaskedNumber(
372 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]
376 // Decryption failure of existing record should not prevent it from being updated.
377 creditCard = profileStorage.creditCards._data[0];
378 creditCard["cc-number-encrypted"] = "INVALID";
379 await profileStorage.creditCards.update(
380 profileStorage.creditCards._data[0].guid,
381 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD,
384 creditCard = profileStorage.creditCards._data[0];
386 creditCard["cc-number"],
387 CreditCard.getLongMaskedNumber(
388 TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]
392 await Assert.rejects(
393 profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
394 /No matching record\./
397 await Assert.rejects(
398 profileStorage.creditCards.update(
400 TEST_CREDIT_CARD_WITH_INVALID_FIELD
402 /"cc-type" contains invalid data type: object/
405 await Assert.rejects(
406 profileStorage.creditCards.update(guid, {}),
407 /Record contains no valid field\./
410 await Assert.rejects(
411 profileStorage.creditCards.update(
413 TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE
415 /Record contains no valid field\./
418 await profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_1);
419 await Assert.rejects(
420 profileStorage.creditCards.update(
422 TEST_CREDIT_CARD_EMPTY_AFTER_UPDATE_CREDIT_CARD_1
424 /Record contains no valid field\./
428 add_task(async function test_validate() {
429 let path = getTempFile(TEST_STORE_FILE_NAME).path;
431 let profileStorage = new FormAutofillStorage(path);
432 await profileStorage.initialize();
434 await profileStorage.creditCards.add(
435 TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE
437 await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR);
438 await profileStorage.creditCards.add(
439 TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS
441 let creditCards = await profileStorage.creditCards.getAll();
443 Assert.equal(creditCards[0]["cc-exp-month"], undefined);
444 Assert.equal(creditCards[0]["cc-exp-year"], undefined);
445 Assert.equal(creditCards[0]["cc-exp"], undefined);
447 let month = TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-month"];
449 parseInt(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-year"], 10) + 2000;
450 Assert.equal(creditCards[1]["cc-exp-month"], month);
451 Assert.equal(creditCards[1]["cc-exp-year"], year);
453 creditCards[1]["cc-exp"],
454 year + "-" + month.toString().padStart(2, "0")
457 Assert.equal(creditCards[2]["cc-number"].length, 16);
460 add_task(async function test_notifyUsed() {
461 let path = getTempFile(TEST_STORE_FILE_NAME).path;
462 await prepareTestCreditCards(path);
464 let profileStorage = new FormAutofillStorage(path);
465 await profileStorage.initialize();
467 let creditCards = await profileStorage.creditCards.getAll();
468 let guid = creditCards[1].guid;
469 let timeLastUsed = creditCards[1].timeLastUsed;
470 let timesUsed = creditCards[1].timesUsed;
472 let onChanged = TestUtils.topicObserved(
473 "formautofill-storage-changed",
475 data == "notifyUsed" &&
476 subject.wrappedJSObject.collectionName == COLLECTION_NAME &&
477 subject.wrappedJSObject.guid == guid
480 profileStorage.creditCards.notifyUsed(guid);
482 await profileStorage._saveImmediately();
484 profileStorage = new FormAutofillStorage(path);
485 await profileStorage.initialize();
487 let creditCard = await profileStorage.creditCards.get(guid);
489 Assert.equal(creditCard.timesUsed, timesUsed + 1);
490 Assert.notEqual(creditCard.timeLastUsed, timeLastUsed);
493 () => profileStorage.creditCards.notifyUsed("INVALID_GUID"),
494 /No matching record\./
498 add_task(async function test_remove() {
499 let path = getTempFile(TEST_STORE_FILE_NAME).path;
500 await prepareTestCreditCards(path);
502 let profileStorage = new FormAutofillStorage(path);
503 await profileStorage.initialize();
505 let creditCards = await profileStorage.creditCards.getAll();
506 let guid = creditCards[1].guid;
508 let onChanged = TestUtils.topicObserved(
509 "formautofill-storage-changed",
512 subject.wrappedJSObject.guid == guid &&
513 subject.wrappedJSObject.collectionName == COLLECTION_NAME
516 Assert.equal(creditCards.length, 2);
518 profileStorage.creditCards.remove(guid);
520 await profileStorage._saveImmediately();
522 profileStorage = new FormAutofillStorage(path);
523 await profileStorage.initialize();
525 creditCards = await profileStorage.creditCards.getAll();
527 Assert.equal(creditCards.length, 1);
529 Assert.equal(await profileStorage.creditCards.get(guid), null);
532 add_task(async function test_getDuplicateRecords() {
533 let profileStorage = await initProfileStorage(
534 TEST_STORE_FILE_NAME,
535 [TEST_CREDIT_CARD_3],
538 let guid = profileStorage.creditCards._data[0].guid;
540 // Absolutely a duplicate.
541 let getDuplicateRecords =
542 profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_3);
543 let dupe = (await getDuplicateRecords.next()).value;
544 Assert.equal(dupe.guid, guid);
546 // Absolutely not a duplicate.
547 getDuplicateRecords =
548 profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_1);
549 dupe = (await getDuplicateRecords.next()).value;
550 Assert.equal(dupe, null);
552 // Subset with the same number is a duplicate.
553 let record = Object.assign({}, TEST_CREDIT_CARD_3);
554 delete record["cc-exp-month"];
555 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
556 dupe = (await getDuplicateRecords.next()).value;
557 Assert.equal(dupe.guid, guid);
559 // Superset with the same number is a duplicate.
560 record = Object.assign({}, TEST_CREDIT_CARD_3);
561 record["cc-name"] = "John Doe";
562 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
563 dupe = (await getDuplicateRecords.next()).value;
564 Assert.equal(dupe.guid, guid);
566 // Numbers with the same last 4 digits shouldn't be treated as a duplicate.
567 record = Object.assign({}, TEST_CREDIT_CARD_3);
568 let last4Digits = record["cc-number"].substr(-4);
569 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
570 dupe = (await getDuplicateRecords.next()).value;
571 Assert.equal(dupe.guid, guid);
573 // This number differs from TEST_CREDIT_CARD_3 by swapping the order of the
574 // 09 and 90 adjacent digits, which is still a valid credit card number.
575 record["cc-number"] = "358999378390" + last4Digits;
577 // We don't treat numbers with the same last 4 digits as a duplicate.
578 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
579 dupe = (await getDuplicateRecords.next()).value;
580 Assert.equal(dupe, null);
583 add_task(async function test_getDuplicateRecordsMatch() {
584 let profileStorage = await initProfileStorage(
585 TEST_STORE_FILE_NAME,
586 [TEST_CREDIT_CARD_2],
589 let guid = profileStorage.creditCards._data[0].guid;
591 // Absolutely a duplicate.
592 let getDuplicateRecords =
593 profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_2);
594 let dupe = (await getDuplicateRecords.next()).value;
595 Assert.equal(dupe.guid, guid);
597 // Absolutely not a duplicate.
598 getDuplicateRecords =
599 profileStorage.creditCards.getDuplicateRecords(TEST_CREDIT_CARD_1);
600 dupe = (await getDuplicateRecords.next()).value;
601 Assert.equal(dupe, null);
603 record = Object.assign({}, TEST_CREDIT_CARD_2);
605 // We change month from `1` to `2`
606 record["cc-exp-month"] = 2;
607 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
608 dupe = (await getDuplicateRecords.next()).value;
609 Assert.equal(dupe.guid, guid);
611 // We change year from `2000` to `2001`
612 record["cc-exp-year"] = 2001;
613 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
614 dupe = (await getDuplicateRecords.next()).value;
615 Assert.equal(dupe.guid, guid);
617 // New name, same card
618 record["cc-name"] = "John Doe";
619 getDuplicateRecords = profileStorage.creditCards.getDuplicateRecords(record);
620 dupe = (await getDuplicateRecords.next()).value;
621 Assert.equal(dupe.guid, guid);
624 add_task(async function test_getMatchRecord() {
625 let profileStorage = await initProfileStorage(
626 TEST_STORE_FILE_NAME,
627 [TEST_CREDIT_CARD_2],
630 let guid = profileStorage.creditCards._data[0].guid;
632 const TEST_FIELDS = {
633 "cc-name": "John Doe",
638 // Absolutely a match.
639 let getMatchRecords =
640 profileStorage.creditCards.getMatchRecords(TEST_CREDIT_CARD_2);
641 let match = (await getMatchRecords.next()).value;
642 Assert.equal(match.guid, guid);
644 // Subset with the same number is a match.
645 for (const field of Object.keys(TEST_FIELDS)) {
646 let record = Object.assign({}, TEST_CREDIT_CARD_2);
647 delete record[field];
648 getMatchRecords = profileStorage.creditCards.getMatchRecords(record);
649 match = (await getMatchRecords.next()).value;
650 Assert.equal(match.guid, guid);
653 // Subset with different number is not a match.
654 for (const field of Object.keys(TEST_FIELDS)) {
655 let record = Object.assign({}, TEST_CREDIT_CARD_2, {
656 "cc-number": TEST_CREDIT_CARD_1["cc-number"],
658 delete record[field];
659 getMatchRecords = profileStorage.creditCards.getMatchRecords(record);
660 match = (await getMatchRecords.next()).value;
661 Assert.equal(match, null);
664 // Superset with the same number is not a match.
665 for (const [field, value] of Object.entries(TEST_FIELDS)) {
666 let record = Object.assign({}, TEST_CREDIT_CARD_2);
667 record[field] = value;
668 getMatchRecords = profileStorage.creditCards.getMatchRecords(record);
669 match = (await getMatchRecords.next()).value;
670 Assert.equal(match, null);
673 // Superset with different number is not a match.
674 for (const [field, value] of Object.entries(TEST_FIELDS)) {
675 let record = Object.assign({}, TEST_CREDIT_CARD_2, {
676 "cc-number": TEST_CREDIT_CARD_1["cc-number"],
678 record[field] = value;
679 getMatchRecords = profileStorage.creditCards.getMatchRecords(record);
680 match = (await getMatchRecords.next()).value;
681 Assert.equal(match, null);
685 add_task(async function test_creditCardFillDisabled() {
686 Services.prefs.setBoolPref(
687 "extensions.formautofill.creditCards.enabled",
691 let path = getTempFile(TEST_STORE_FILE_NAME).path;
692 let profileStorage = new FormAutofillStorage(path);
693 await profileStorage.initialize();
696 !!profileStorage.creditCards,
698 "credit card records initialized and available."
701 Services.prefs.setBoolPref(
702 "extensions.formautofill.creditCards.enabled",