Bug 1415951 - Extend TPS to support Addresses and Credit Cards. r?tcsc draft
authorEdouard Oger <eoger@fastmail.com>
Mon, 05 Feb 2018 14:39:32 -0600
changeset 752795 7b2764eae6d894e1c1ba8a636fc5213e30875671
parent 752727 c5120bcaf7bdcb5cdb06a02b60bd5bfe6a867d06
push id98392
push userbmo:eoger@fastmail.com
push dateFri, 09 Feb 2018 00:07:24 +0000
reviewerstcsc
bugs1415951
milestone60.0a1
Bug 1415951 - Extend TPS to support Addresses and Credit Cards. r?tcsc MozReview-Commit-ID: 8VKBvm60ZSH
services/sync/tests/tps/.eslintrc.js
services/sync/tests/tps/all_tests.json
services/sync/tests/tps/test_addresses.js
services/sync/tests/tps/test_creditcards.js
services/sync/tests/unit/sync_ping_schema.json
services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
services/sync/tps/extensions/tps/resource/tps.jsm
tools/lint/eslint/modules.json
--- a/services/sync/tests/tps/.eslintrc.js
+++ b/services/sync/tests/tps/.eslintrc.js
@@ -3,17 +3,19 @@
 module.exports = {
   "extends": [
     "plugin:mozilla/mochitest-test"
   ],
 
   globals: {
     // Injected into tests via tps.jsm
     "Addons": false,
+    "Addresses": false,
     "Bookmarks": false,
+    "CreditCards": false,
     "EnableEngines": false,
     "EnsureTracking": false,
     "Formdata": false,
     "History": false,
     "Login": false,
     "Passwords": false,
     "Phase": false,
     "Prefs": false,
--- a/services/sync/tests/tps/all_tests.json
+++ b/services/sync/tests/tps/all_tests.json
@@ -20,13 +20,15 @@
     "test_privbrw_tabs.js",
     "test_bookmarks_in_same_named_folder.js",
     "test_client_wipe.js",
     "test_special_tabs.js",
     "test_addon_restartless_xpi.js",
     "test_addon_nonrestartless_xpi.js",
     "test_addon_reconciling.js",
     "test_addon_wipe.js",
-    "test_existing_bookmarks.js"
+    "test_existing_bookmarks.js",
+    "test_addresses.js",
+    "test_creditcards.js",
   ]
 }
 
 
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_addresses.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global Services */
+Services.prefs.setBoolPref("services.sync.engine.addresses", true);
+
+EnableEngines(["addresses"]);
+
+var phases = {
+  "phase1": "profile1",
+  "phase2": "profile2",
+  "phase3": "profile1"
+};
+
+const address1 = [{
+  "given-name": "Timothy",
+  "additional-name": "John",
+  "family-name": "Berners-Lee",
+  "organization": "World Wide Web Consortium",
+  "street-address": "32 Vassar Street\nMIT Room 32-G524",
+  "address-level2": "Cambridge",
+  "address-level1": "MA",
+  "postal-code": "02139",
+  "country": "US",
+  "tel": "+16172535702",
+  "email": "timbl@w3.org",
+  changes: {
+    "organization": "W3C"
+  }
+}];
+
+const address1_after = [{
+  "given-name": "Timothy",
+  "additional-name": "John",
+  "family-name": "Berners-Lee",
+  "organization": "W3C",
+  "street-address": "32 Vassar Street\nMIT Room 32-G524",
+  "address-level2": "Cambridge",
+  "address-level1": "MA",
+  "postal-code": "02139",
+  "country": "US",
+  "tel": "+16172535702",
+  "email": "timbl@w3.org",
+}];
+
+const address2 = [{
+  "given-name": "John",
+  "additional-name": "R.",
+  "family-name": "Smith",
+  "organization": "Mozilla",
+  "street-address": "Geb\u00E4ude 3, 4. Obergeschoss\nSchlesische Stra\u00DFe 27",
+  "address-level2": "Berlin",
+  "address-level1": "BE",
+  "postal-code": "10997",
+  "country": "DE",
+  "tel": "+4930983333000",
+  "email": "timbl@w3.org",
+}];
+
+Phase("phase1", [
+  [Addresses.add, address1],
+  [Sync]
+]);
+
+Phase("phase2", [
+  [Sync],
+  [Addresses.verify, address1],
+  [Addresses.modify, address1],
+  [Addresses.add, address2],
+  [Sync]
+]);
+
+Phase("phase3", [
+  [Sync],
+  [Addresses.verify, address1_after],
+  [Addresses.verify, address2]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_creditcards.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global Services */
+Services.prefs.setBoolPref("services.sync.engine.creditcards", true);
+
+EnableEngines(["creditcards"]);
+
+var phases = {
+  "phase1": "profile1",
+  "phase2": "profile2",
+  "phase3": "profile1"
+};
+
+const cc1 = [{
+  "cc-name": "John Doe",
+  "cc-number": "1234567812345678",
+  "cc-exp-month": 4,
+  "cc-exp-year": 2017,
+  "changes": {
+    "cc-exp-year": 2018
+  }
+}];
+
+const cc1_after = [{
+  "cc-name": "John Doe",
+  "cc-number": "1234567812345678",
+  "cc-exp-month": 4,
+  "cc-exp-year": 2018,
+}];
+
+const cc2 = [{
+  "cc-name": "Timothy Berners-Lee",
+  "cc-number": "1111222233334444",
+  "cc-exp-month": 12,
+  "cc-exp-year": 2022,
+}];
+
+Phase("phase1", [
+  [CreditCards.add, cc1],
+  [Sync]
+]);
+
+Phase("phase2", [
+  [Sync],
+  [CreditCards.verify, cc1],
+  [CreditCards.modify, cc1],
+  [CreditCards.add, cc2],
+  [Sync]
+]);
+
+Phase("phase3", [
+  [Sync],
+  [CreditCards.verifyNot, cc1],
+  [CreditCards.verify, cc1_after],
+  [CreditCards.verify, cc2]
+]);
--- a/services/sync/tests/unit/sync_ping_schema.json
+++ b/services/sync/tests/unit/sync_ping_schema.json
@@ -73,17 +73,17 @@
         "version": { "type": "string" }
       }
     },
     "engine": {
       "required": ["name"],
       "additionalProperties": false,
       "properties": {
         "failureReason": { "$ref": "#/definitions/error" },
-        "name": { "enum": ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs", "extension-storage"] },
+        "name": { "type": "string" },
         "took": { "type": "integer", "minimum": 1 },
         "status": { "type": "string" },
         "incoming": {
           "type": "object",
           "additionalProperties": false,
           "anyOf": [
             {"required": ["applied"]},
             {"required": ["failed"]},
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /* This is a JavaScript module (JSM) to be imported via
+  * Components.utils.import() and acts as a singleton. Only the following
+  * listed symbols will exposed on import, and only when and where imported.
+  */
+
+var EXPORTED_SYMBOLS = ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"];
+
+ChromeUtils.import("resource://tps/logger.jsm");
+ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm");
+ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+
+class FormAutofillBase {
+  constructor(props, subStorageName, fields) {
+    this._subStorageName = subStorageName;
+    this._fields = fields;
+
+    this.props = {};
+    this.updateProps = null;
+    if ("changes" in props) {
+      this.updateProps = props.changes;
+    }
+    for (const field of this._fields) {
+      this.props[field] = (field in props) ? props[field] : null;
+    }
+  }
+
+  get storage() {
+    return profileStorage[this._subStorageName];
+  }
+
+  Create() {
+    this.storage.add(this.props);
+  }
+
+  Find() {
+    return this.storage._data.find(entry =>
+      this._fields.every(field => entry[field] === this.props[field])
+    );
+  }
+
+  Update() {
+    const {guid} = this.Find();
+    this.storage.update(guid, this.updateProps, true);
+  }
+
+  Remove() {
+    const {guid} = this.Find();
+    this.storage.remove(guid);
+  }
+}
+
+function DumpStorage(subStorageName) {
+  Logger.logInfo(`\ndumping ${subStorageName} list\n`, true);
+  const entries = profileStorage[subStorageName]._data;
+  for (const entry of entries) {
+    Logger.logInfo(JSON.stringify(entry), true);
+  }
+  Logger.logInfo(`\n\nend ${subStorageName} list\n`, true);
+}
+
+const ADDRESS_FIELDS = [
+  "given-name",
+  "additional-name",
+  "family-name",
+  "organization",
+  "street-address",
+  "address-level2",
+  "address-level1",
+  "postal-code",
+  "country",
+  "tel",
+  "email",
+];
+
+class Address extends FormAutofillBase {
+  constructor(props) {
+    super(props, "addresses", ADDRESS_FIELDS);
+  }
+}
+
+function DumpAddresses() {
+  DumpStorage("addresses");
+}
+
+const CREDIT_CARD_FIELDS = [
+  "cc-name",
+  "cc-number",
+  "cc-exp-month",
+  "cc-exp-year",
+];
+
+class CreditCard extends FormAutofillBase {
+  constructor(props) {
+    super(props, "creditCards", CREDIT_CARD_FIELDS);
+  }
+
+  Find() {
+    return this.storage._data.find(entry => {
+      entry["cc-number"] = MasterPassword.decryptSync(entry["cc-number-encrypted"]);
+      return this._fields.every(field => entry[field] === this.props[field]);
+    });
+  }
+}
+
+function DumpCreditCards() {
+  DumpStorage("creditCards");
+}
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -7,16 +7,17 @@
   * listed symbols will exposed on import, and only when and where imported.
   */
 
 var EXPORTED_SYMBOLS = ["ACTIONS", "TPS"];
 
 var module = this;
 
 // Global modules
+ChromeUtils.import("resource://formautofill/FormAutofillSync.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
 ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
@@ -32,16 +33,17 @@ ChromeUtils.import("resource://services-
 ChromeUtils.import("resource://services-sync/engines/forms.js");
 ChromeUtils.import("resource://services-sync/engines/addons.js");
 // TPS modules
 ChromeUtils.import("resource://tps/logger.jsm");
 
 // Module wrappers for tests
 ChromeUtils.import("resource://tps/modules/addons.jsm");
 ChromeUtils.import("resource://tps/modules/bookmarks.jsm");
+ChromeUtils.import("resource://tps/modules/formautofill.jsm");
 ChromeUtils.import("resource://tps/modules/forms.jsm");
 ChromeUtils.import("resource://tps/modules/history.jsm");
 ChromeUtils.import("resource://tps/modules/passwords.jsm");
 ChromeUtils.import("resource://tps/modules/prefs.jsm");
 ChromeUtils.import("resource://tps/modules/tabs.jsm");
 ChromeUtils.import("resource://tps/modules/windows.jsm");
 
 var hh = Cc["@mozilla.org/network/protocol;1?name=http"]
@@ -570,16 +572,88 @@ var TPS = {
       Logger.logPass("executing action " + action.toUpperCase() +
         " on bookmarks");
     } catch (e) {
       await DumpBookmarks();
       throw (e);
     }
   },
 
+  async HandleAddresses(addresses, action) {
+    try {
+      for (let address of addresses) {
+        Logger.logInfo("executing action " + action.toUpperCase() +
+                      " on address " + JSON.stringify(address));
+        let addressOb = new Address(address);
+        switch (action) {
+          case ACTION_ADD:
+            addressOb.Create();
+            break;
+          case ACTION_MODIFY:
+            addressOb.Update();
+            break;
+          case ACTION_VERIFY:
+            Logger.AssertTrue(addressOb.Find(), "address not found");
+            break;
+          case ACTION_VERIFY_NOT:
+            Logger.AssertTrue(!addressOb.Find(),
+              "address found, but it shouldn't exist");
+            break;
+          case ACTION_DELETE:
+            Logger.AssertTrue(addressOb.Find(), "address not found");
+            addressOb.Remove();
+            break;
+          default:
+            Logger.AssertTrue(false, "invalid action: " + action);
+        }
+      }
+      Logger.logPass("executing action " + action.toUpperCase() +
+                     " on addresses");
+    } catch (e) {
+      DumpAddresses();
+      throw (e);
+    }
+  },
+
+  async HandleCreditCards(creditCards, action) {
+    try {
+      for (let creditCard of creditCards) {
+        Logger.logInfo("executing action " + action.toUpperCase() +
+                      " on creditCard " + JSON.stringify(creditCard));
+        let creditCardOb = new CreditCard(creditCard);
+        switch (action) {
+          case ACTION_ADD:
+            creditCardOb.Create();
+            break;
+          case ACTION_MODIFY:
+            creditCardOb.Update();
+            break;
+          case ACTION_VERIFY:
+            Logger.AssertTrue(creditCardOb.Find(), "creditCard not found");
+            break;
+          case ACTION_VERIFY_NOT:
+            Logger.AssertTrue(!creditCardOb.Find(),
+              "creditCard found, but it shouldn't exist");
+            break;
+          case ACTION_DELETE:
+            Logger.AssertTrue(creditCardOb.Find(), "creditCard not found");
+            creditCardOb.Remove();
+            break;
+          default:
+            Logger.AssertTrue(false, "invalid action: " + action);
+        }
+      }
+      Logger.logPass("executing action " + action.toUpperCase() +
+                     " on creditCards");
+    } catch (e) {
+      DumpCreditCards();
+      throw (e);
+    }
+  },
+
   async Cleanup() {
     try {
       await this.WipeServer();
     } catch (ex) {
       Logger.logError("Failed to wipe server: " + Log.exceptionStr(ex));
     }
     try {
       if (await Authentication.isLoggedIn()) {
@@ -1186,16 +1260,34 @@ var Addons = {
   async verifyNot(addons) {
     await TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
   },
   skipValidation() {
     TPS.shouldValidateAddons = false;
   }
 };
 
+var Addresses = {
+  async add(addresses) {
+    await this.HandleAddresses(addresses, ACTION_ADD);
+  },
+  async modify(addresses) {
+    await this.HandleAddresses(addresses, ACTION_MODIFY);
+  },
+  async delete(addresses) {
+    await this.HandleAddresses(addresses, ACTION_DELETE);
+  },
+  async verify(addresses) {
+    await this.HandleAddresses(addresses, ACTION_VERIFY);
+  },
+  async verifyNot(addresses) {
+    await this.HandleAddresses(addresses, ACTION_VERIFY_NOT);
+  }
+};
+
 var Bookmarks = {
   async add(bookmarks) {
     await TPS.HandleBookmarks(bookmarks, ACTION_ADD);
   },
   async modify(bookmarks) {
     await TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
   },
   async delete(bookmarks) {
@@ -1207,16 +1299,34 @@ var Bookmarks = {
   async verifyNot(bookmarks) {
     await TPS.HandleBookmarks(bookmarks, ACTION_VERIFY_NOT);
   },
   skipValidation() {
     TPS.shouldValidateBookmarks = false;
   }
 };
 
+var CreditCards = {
+  async add(creditCards) {
+    await this.HandleCreditCards(creditCards, ACTION_ADD);
+  },
+  async modify(creditCards) {
+    await this.HandleCreditCards(creditCards, ACTION_MODIFY);
+  },
+  async delete(creditCards) {
+    await this.HandleCreditCards(creditCards, ACTION_DELETE);
+  },
+  async verify(creditCards) {
+    await this.HandleCreditCards(creditCards, ACTION_VERIFY);
+  },
+  async verifyNot(creditCards) {
+    await this.HandleCreditCards(creditCards, ACTION_VERIFY_NOT);
+  }
+};
+
 var Formdata = {
   async add(formdata) {
     await this.HandleForms(formdata, ACTION_ADD);
   },
   async delete(formdata) {
     await this.HandleForms(formdata, ACTION_DELETE);
   },
   async verify(formdata) {
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -63,16 +63,17 @@
   "ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"],
   "NativeManifests.jsm": ["NativeManifests"],
   "fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"],
   "file_expandosharing.jsm": ["checkFromJSM"],
   "file_stringencoding.jsm": ["checkFromJSM"],
   "file_url.jsm": ["checkFromJSM"],
   "file_worker_url.jsm": ["checkFromJSM"],
   "Finder.jsm": ["Finder", "GetClipboardSearchString"],
+  "formautofill.jsm": ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"],
   "forms.js": ["FormEngine", "FormRec", "FormValidator"],
   "forms.jsm": ["FormData"],
   "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
   "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
   "FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"],
   "FrameScriptManager.jsm": ["getNewLoaderID"],
   "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
   "fxaccounts.jsm": ["Authentication"],