Bug 1434508 - better serialization of paymentrequest data. r?MattN draft
authorSam Foster <sfoster@mozilla.com>
Wed, 31 Jan 2018 16:19:49 -0800
changeset 752880 d90cbd021c482a18bbc1b8682bac2a6acf9ea760
parent 752727 c5120bcaf7bdcb5cdb06a02b60bd5bfe6a867d06
push id98406
push userbmo:sfoster@mozilla.com
push dateFri, 09 Feb 2018 01:56:13 +0000
reviewersMattN
bugs1434508
milestone60.0a1
Bug 1434508 - better serialization of paymentrequest data. r?MattN * Add a serializeRequest method to paymentDialogWrapper to correctly serialize the nsIArray values * Test results of serializing a request with multiple displayItems, shippingOptions, paymentMethods and modifiers MozReview-Commit-ID: DTqzTAjvdxq
toolkit/components/payments/content/paymentDialogWrapper.js
toolkit/components/payments/test/PaymentTestUtils.jsm
toolkit/components/payments/test/browser/browser.ini
toolkit/components/payments/test/browser/browser_request_serialization.js
--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -197,26 +197,95 @@ var paymentDialogWrapper = {
       messageType: "updateState",
       data: {
         savedAddresses: this.fetchSavedAddresses(),
         savedBasicCards: this.fetchSavedPaymentCards(),
       },
     });
   },
 
-  initializeFrame() {
-    let requestSerialized = JSON.parse(JSON.stringify(this.request));
+  /**
+   * Recursively convert and filter input to the subset of data types supported by JSON
+   *
+   * @param {*} value - any type of input to serialize
+   * @param {string?} name - name or key associated with this input.
+   *                         E.g. property name or array index.
+   * @returns {*} serialized deep copy of the value
+   */
+  _serializeRequest(value, name = null) {
+    // Primitives: String, Number, Boolean, null
+    let type = typeof value;
+    if (value === null ||
+        type == "string" ||
+        type == "number" ||
+        type == "boolean") {
+      return value;
+    }
+    if (name == "topLevelPrincipal") {
+      // Manually serialize the nsIPrincipal.
+      let displayHost = value.URI.displayHost;
+      return {
+        URI: {
+          displayHost,
+        },
+      };
+    }
+    if (type == "function" || type == "undefined") {
+      return undefined;
+    }
+    // Structures: nsIArray
+    if (value instanceof Ci.nsIArray) {
+      let iface;
+      let items = [];
+      switch (name) {
+        case "displayItems": // falls through
+        case "additionalDisplayItems":
+          iface = Ci.nsIPaymentItem;
+          break;
+        case "shippingOptions":
+          iface = Ci.nsIPaymentShippingOption;
+          break;
+        case "paymentMethods":
+          iface = Ci.nsIPaymentMethodData;
+          break;
+        case "modifiers":
+          iface = Ci.nsIPaymentDetailsModifier;
+          break;
+      }
+      if (!iface) {
+        throw new Error(`No interface associated with the members of the ${name} nsIArray`);
+      }
+      for (let i = 0; i < value.length; i++) {
+        let item = value.queryElementAt(i, iface);
+        let result = this._serializeRequest(item, i);
+        if (result !== undefined) {
+          items.push(result);
+        }
+      }
+      return items;
+    }
+    // Structures: Arrays
+    if (Array.isArray(value)) {
+      let items = value.map(item => { this._serializeRequest(item); })
+                       .filter(item => item !== undefined);
+      return items;
+    }
+    // Structures: Objects
+    let obj = {};
+    for (let [key, item] of Object.entries(value)) {
+      let result = this._serializeRequest(item, key);
+      if (result !== undefined) {
+        obj[key] = result;
+      }
+    }
+    return obj;
+  },
 
-    // Manually serialize the nsIPrincipal.
-    let displayHost = this.request.topLevelPrincipal.URI.displayHost;
-    requestSerialized.topLevelPrincipal = {
-      URI: {
-        displayHost,
-      },
-    };
+  initializeFrame() {
+    let requestSerialized = this._serializeRequest(this.request);
 
     this.mm.sendAsyncMessage("paymentChromeToContent", {
       messageType: "showPaymentRequest",
       data: {
         request: requestSerialized,
         savedAddresses: this.fetchSavedAddresses(),
         savedBasicCards: this.fetchSavedPaymentCards(),
       },
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -74,22 +74,103 @@ this.PaymentTestUtils = {
 
   /**
    * Common PaymentMethodData for testing
    */
   MethodData: {
     basicCard: {
       supportedMethods: "basic-card",
     },
+    bobPay: {
+      supportedMethods: "https://www.example.com/bobpay",
+    },
   },
 
   /**
    * Common PaymentDetailsInit for testing
    */
   Details: {
     total60USD: {
       total: {
         label: "Total due",
         amount: { currency: "USD", value: "60.00" },
       },
     },
+    twoDisplayItems: {
+      total: {
+        label: "Total due",
+        amount: { currency: "USD", value: "32.00" },
+      },
+      displayItems: [
+        {
+          label: "First",
+          amount: { currency: "USD", value: "1" },
+        },
+        {
+          label: "Second",
+          amount: { currency: "USD", value: "2" },
+        },
+      ],
+    },
+    twoShippingOptions: {
+      total: {
+        label: "Total due",
+        amount: { currency: "USD", value: "2.00" },
+      },
+      shippingOptions: [
+        {
+          id: "1",
+          label: "Meh Unreliable Shipping",
+          amount: { currency: "USD", value: "1" },
+        },
+        {
+          id: "2",
+          label: "Premium Slow Shipping",
+          amount: { currency: "USD", value: "2" },
+          selected: true,
+        },
+      ],
+    },
+    bobPayPaymentModifier: {
+      total: {
+        label: "Total due",
+        amount: { currency: "USD", value: "2.00" },
+      },
+      displayItems: [
+        {
+          label: "First",
+          amount: { currency: "USD", value: "1" },
+        },
+      ],
+      modifiers: [
+        {
+          additionalDisplayItems: [
+            {
+              label: "Credit card fee",
+              amount: { currency: "USD", value: "0.50" },
+            },
+          ],
+          supportedMethods: "basic-card",
+          total: {
+            label: "Total due",
+            amount: { currency: "USD", value: "2.50" },
+          },
+          data: {
+            supportedTypes: "credit",
+          },
+        },
+        {
+          additionalDisplayItems: [
+            {
+              label: "Bob-pay fee",
+              amount: { currency: "USD", value: "1.50" },
+            },
+          ],
+          supportedMethods: "https://www.example.com/bobpay",
+          total: {
+            label: "Total due",
+            amount: { currency: "USD", value: "3.50" },
+          },
+        },
+      ],
+    },
   },
 };
--- a/toolkit/components/payments/test/browser/browser.ini
+++ b/toolkit/components/payments/test/browser/browser.ini
@@ -3,12 +3,13 @@ head = head.js
 prefs =
   dom.payments.request.enabled=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
 [browser_host_name.js]
 [browser_profile_storage.js]
+[browser_request_serialization.js]
 [browser_request_summary.js]
 [browser_show_dialog.js]
 skip-if = os == 'win' && debug # bug 1418385
 [browser_total.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/browser/browser_request_serialization.js
@@ -0,0 +1,121 @@
+"use strict";
+
+// Disable CPOW checks because they have false-positives from use of ContentTask in a helper.
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+add_task(async function test_serializeRequest_displayItems() {
+  const testTask = ({methodData, details}) => {
+    let contentWin = Components.utils.waiveXrays(content);
+    let store = contentWin.document.querySelector("payment-dialog").requestStore;
+    let state = store && store.getState();
+    ok(state, "got request store state");
+
+    let expected = details;
+    let actual = state.request.paymentDetails;
+    if (expected.displayItems) {
+      is(actual.displayItems.length, expected.displayItems.length, "displayItems have same length");
+      for (let i = 0; i < actual.displayItems.length; i++) {
+        let item = actual.displayItems[i], expectedItem = expected.displayItems[i];
+        is(item.label, expectedItem.label, "displayItem label matches");
+        is(item.amount.value, expectedItem.amount.value, "displayItem label matches");
+        is(item.amount.currency, expectedItem.amount.currency, "displayItem label matches");
+      }
+    } else {
+      is(actual.displayItems, null, "falsey input displayItems is serialized to null");
+    }
+  };
+  const args = {
+    methodData: [PTU.MethodData.basicCard],
+    details: PTU.Details.twoDisplayItems,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_shippingOptions() {
+  const testTask = ({methodData, details}) => {
+    let contentWin = Components.utils.waiveXrays(content);
+    let store = contentWin.document.querySelector("payment-dialog").requestStore;
+    let state = store && store.getState();
+    ok(state, "got request store state");
+
+    let expected = details;
+    let actual = state.request.paymentDetails;
+    if (expected.shippingOptions) {
+      is(actual.shippingOptions.length, expected.shippingOptions.length,
+         "shippingOptions have same length");
+      for (let i = 0; i < actual.shippingOptions.length; i++) {
+        let item = actual.shippingOptions[i], expectedItem = expected.shippingOptions[i];
+        is(item.label, expectedItem.label, "shippingOption label matches");
+        is(item.amount.value, expectedItem.amount.value, "shippingOption label matches");
+        is(item.amount.currency, expectedItem.amount.currency, "shippingOption label matches");
+      }
+    } else {
+      is(actual.shippingOptions, null, "falsey input shippingOptions is serialized to null");
+    }
+  };
+
+  const args = {
+    methodData: [PTU.MethodData.basicCard],
+    details: PTU.Details.twoShippingOptions,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_paymentMethods() {
+  const testTask = ({methodData, details}) => {
+    let contentWin = Components.utils.waiveXrays(content);
+    let store = contentWin.document.querySelector("payment-dialog").requestStore;
+    let state = store && store.getState();
+    ok(state, "got request store state");
+
+    let result = state.request;
+
+    is(result.paymentMethods.length, 2, "Correct number of payment methods");
+    ok(result.paymentMethods[0].supportedMethods && result.paymentMethods[1].supportedMethods,
+       "Both payment methods look valid");
+  };
+  const args = {
+    methodData: [PTU.MethodData.basicCard, PTU.MethodData.bobPay],
+    details: PTU.Details.total60USD,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_modifiers() {
+  const testTask = ({methodData, details}) => {
+    let contentWin = Components.utils.waiveXrays(content);
+    let store = contentWin.document.querySelector("payment-dialog").requestStore;
+    let state = store && store.getState();
+    ok(state, "got request store state");
+
+    let expected = details;
+    let actual = state.request.paymentDetails;
+
+    is(actual.modifiers.length, expected.modifiers.length,
+       "modifiers have same length");
+    for (let i = 0; i < actual.modifiers.length; i++) {
+      let item = actual.modifiers[i], expectedItem = expected.modifiers[i];
+      is(item.supportedMethods, expectedItem.supportedMethods, "modifier supportedMethods matches");
+
+      is(item.additionalDisplayItems[0].label, expectedItem.additionalDisplayItems[0].label,
+         "additionalDisplayItems label matches");
+      is(item.additionalDisplayItems[0].amount.value,
+         expectedItem.additionalDisplayItems[0].amount.value,
+         "additionalDisplayItems amount value matches");
+      is(item.additionalDisplayItems[0].amount.currency,
+         expectedItem.additionalDisplayItems[0].amount.currency,
+         "additionalDisplayItems amount currency matches");
+
+      is(item.total.label, expectedItem.total.label, "modifier total label matches");
+      is(item.total.amount.value, expectedItem.total.amount.value, "modifier label matches");
+      is(item.total.amount.currency, expectedItem.total.amount.currency,
+         "modifier total currency matches");
+    }
+  };
+
+  const args = {
+    methodData: [PTU.MethodData.bobPay],
+    details: PTU.Details.bobPayPaymentModifier,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});