Bug 1255894: Part 8 - Add tests for response stream filtering. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 23 Mar 2017 12:09:26 -0700
changeset 653836 66b9549a6b8ba80d5411f6622008fc913ec17f38
parent 653835 8c6617c8b30fd9b4ebe8e4a57e14818dfe1d0027
child 653837 5063880c34607d9fa6cffce2328e26c5c4501e4f
push id76425
push usermaglione.k@gmail.com
push dateMon, 28 Aug 2017 04:09:59 +0000
reviewersmixedpuppy
bugs1255894
milestone57.0a1
Bug 1255894: Part 8 - Add tests for response stream filtering. r?mixedpuppy MozReview-Commit-ID: 9C2QnNsm1W1
toolkit/components/extensions/ExtensionTestCommon.jsm
toolkit/components/extensions/test/mochitest/lorem.html.gz
toolkit/components/extensions/test/mochitest/lorem.html.gz^headers^
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/slow_response.sjs
toolkit/components/extensions/test/mochitest/test_ext_webrequest_responseBody.html
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -308,37 +308,47 @@ this.ExtensionTestCommon = class Extensi
     }
 
     zipW.close();
 
     return file;
   }
 
   /**
-   * Properly serialize a script into eval-able code string.
+   * Properly serialize a function into eval-able code string.
    *
-   * @param {string|function|Array} script
+   * @param {function} script
    * @returns {string}
    */
-  static serializeScript(script) {
-    if (Array.isArray(script)) {
-      return script.map(this.serializeScript).join(";");
-    }
-    if (typeof script !== "function") {
-      return script;
-    }
+  static serializeFunction(script) {
     // Serialization of object methods doesn't include `function` anymore.
     const method = /^(async )?(\w+)\(/;
 
     let code = script.toString();
     let match = code.match(method);
     if (match && match[2] !== "function") {
       code = code.replace(method, "$1function $2(");
     }
-    return `(${code})();`;
+    return code;
+  }
+
+  /**
+   * Properly serialize a script into eval-able code string.
+   *
+   * @param {string|function|Array} script
+   * @returns {string}
+   */
+  static serializeScript(script) {
+    if (Array.isArray(script)) {
+      return script.map(this.serializeScript, this).join(";");
+    }
+    if (typeof script !== "function") {
+      return script;
+    }
+    return `(${this.serializeFunction(script)})();`;
   }
 
   /**
    * Generates a new extension using |Extension.generateXPI|, and initializes a
    * new |Extension| instance which will execute it.
    *
    * @param {object} data
    * @returns {Extension}
new file mode 100644
index 0000000000000000000000000000000000000000..9eb8d73d501725e50814b0385eac18f9466f95d4
GIT binary patch
literal 392
zc$@)<0eAi%iwFplK+{+N18i?{Wo<5KbZu+^MN&IcqcIHZ^DCCrJX(s72Ds{G?g}Wb
z6t<FR#xHD%fq#E;zAxa$k}d6O*WO({{ds)(_xBkm&1t`ZuiXrhs2m@*T(*}(rnrQj
zWL82rQ6H=i+t-(0H}~7`OBGGL-`y?y(tY>SzI&_jYrpwjJ7-86mJw^J9YYl4$k$%d
z_nT15;GnG5K~b8FAms>7a;X>`y%j7ra*a^V&0(Yji4v_SOkKvC6M=OhVPrZM0wsAj
zx?ONy6<j1c3$o1M!&A!FcsCg+b!fK;{^TA+Nu?H8N|Zxg;AfExnRI7dX<iI=hidl0
zD$Y`zPglaHiA$yo>E<&n`lX<A>wMx8xw{#D@KKXB=VYJBg@ean1WD=QnoBbr>?out
zb1F3Io>*VGo<9ROWt;xGB{c-%;Kjk3MAF&jdRng%xqLf2-E;b2snN6n^>C0Ngvf*Y
myv;SufoS?A?#@`fwtRbrSl@kl?DY?vzu12z{$dYv0ssKRFTaui
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/lorem.html.gz^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html; charset=utf-8
+Content-Encoding: gzip
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -42,17 +42,20 @@ support-files =
   file_simple_xhr_frame.html
   file_simple_xhr_frame2.html
   redirect_auto.sjs
   redirection.sjs
   file_privilege_escalation.html
   file_ext_test_api_injection.js
   file_permission_xhr.html
   file_teardown_test.js
+  lorem.html.gz
+  lorem.html.gz^headers^
   return_headers.sjs
+  slow_response.sjs
   webrequest_worker.js
   !/toolkit/components/passwordmgr/test/authenticate.sjs
   !/dom/tests/mochitest/geolocation/network_geolocation.sjs
 
 [test_ext_clipboard.html]
 # skip-if = # disabled test case with_permission_allow_copy, see inline comment.
 [test_ext_inIncognitoContext_window.html]
 skip-if = os == 'android' # Android does not support multiple windows.
@@ -120,16 +123,17 @@ skip-if = os == 'android'
 [test_ext_web_accessible_resources.html]
 [test_ext_webrequest_auth.html]
 skip-if = os == 'android'
 [test_ext_webrequest_background_events.html]
 [test_ext_webrequest_hsts.html]
 [test_ext_webrequest_basic.html]
 [test_ext_webrequest_filter.html]
 [test_ext_webrequest_frameId.html]
+[test_ext_webrequest_responseBody.html]
 [test_ext_webrequest_suspend.html]
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # Currently fails in emulator tests
 [test_ext_webrequest_permission.html]
 [test_ext_webrequest_websocket.html]
 [test_ext_webnavigation.html]
 [test_ext_webnavigation_filters.html]
 [test_ext_window_postMessage.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/slow_response.sjs
@@ -0,0 +1,55 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */
+"use strict";
+
+/* eslint-disable no-unused-vars */
+
+const DELAY = 200;
+
+const Ci = Components.interfaces;
+
+let nsTimer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
+
+let timer;
+function delay() {
+  return new Promise(resolve => {
+    timer = nsTimer(resolve, DELAY, Ci.nsITimer.TYPE_ONE_SHOT);
+  });
+}
+
+const PARTS = [
+  `<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title></title>
+    </head>
+    <body>`,
+  "Lorem ipsum dolor sit amet, <br>",
+  "consectetur adipiscing elit, <br>",
+  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>",
+  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>",
+  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
+  "Excepteur sint occaecat cupidatat non proident, <br>",
+  "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
+  `
+    </body>
+    </html>`,
+];
+
+async function handleRequest(request, response) {
+  response.processAsync();
+
+  response.setHeader("Content-Type", "text/html", false);
+  response.setHeader("Cache-Control", "no-cache", false);
+
+  await delay();
+
+  for (let part of PARTS) {
+    response.write(`${part}\n`);
+    await delay();
+  }
+
+  response.finish();
+}
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_responseBody.html
@@ -0,0 +1,456 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>WebRequest response body filter test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+"use strict";
+
+const SEQUENTIAL = false;
+
+const PARTS = [
+  `<!DOCTYPE html>
+    <html lang="en">
+    <head>
+      <meta charset="UTF-8">
+      <title></title>
+    </head>
+    <body>`,
+  "Lorem ipsum dolor sit amet, <br>",
+  "consectetur adipiscing elit, <br>",
+  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br>",
+  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br>",
+  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br>",
+  "Excepteur sint occaecat cupidatat non proident, <br>",
+  "sunt in culpa qui officia deserunt mollit anim id est laborum.<br>",
+  `
+    </body>
+    </html>`,
+].map(part => `${part}\n`);
+
+const TIMEOUT = 200;
+const TASKS = [
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let decoder = new TextDecoder("utf-8");
+
+      browser.test.assertEq("uninitialized", filter.status,
+                            `(${num}): Got expected initial status`);
+
+      filter.onstart = event => {
+        browser.test.assertEq("transferringdata", filter.status,
+                              `(${num}): Got expected onStart status`);
+      };
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        let str = decoder.decode(event.data, {stream: true});
+
+        if (n < 3) {
+          browser.test.assertEq(JSON.stringify(PARTS[n]),
+                                JSON.stringify(str),
+                                `(${num}): Got expected part`);
+        }
+        n++;
+
+        filter.write(event.data);
+
+        if (n == 3) {
+          filter.suspend();
+
+          browser.test.assertEq("suspended", filter.status,
+                                `(${num}): Got expected suspended status`);
+
+          let fail = event => {
+            browser.test.fail(`(${num}): Got unexpected data event while suspended`);
+          };
+          filter.addEventListener("data", fail);
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          browser.test.assertEq("suspended", filter.status,
+                                `(${num}): Got expected suspended status`);
+
+          filter.removeEventListener("data", fail);
+          filter.resume();
+          browser.test.assertEq("transferringdata", filter.status,
+                                `(${num}): Got expected resumed status`);
+        } else if (n > 4) {
+          filter.disconnect();
+
+          filter.addEventListener("data", event => {
+            browser.test.fail(`(${num}): Got unexpected data event while disconnected`);
+          });
+
+          browser.test.assertEq("disconnected", filter.status,
+                                `(${num}): Got expected disconnected status`);
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let decoder = new TextDecoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        let str = decoder.decode(event.data, {stream: true});
+
+        if (n < 3) {
+          browser.test.assertEq(JSON.stringify(PARTS[n]),
+                                JSON.stringify(str),
+                                `(${num}): Got expected part`);
+        }
+        n++;
+
+        filter.write(event.data);
+
+        if (n == 3) {
+          filter.suspend();
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          filter.disconnect();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let encoder = new TextEncoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      let n = 0;
+      filter.ondata = async event => {
+        n++;
+
+        filter.write(event.data);
+
+        function checkState(state) {
+          browser.test.assertEq(state, filter.status, `(${num}): Got expected status`);
+        }
+        if (n == 3) {
+          filter.resume();
+          checkState("transferringdata");
+          filter.suspend();
+          checkState("suspended");
+          filter.suspend();
+          checkState("suspended");
+          filter.resume();
+          checkState("transferringdata");
+          filter.suspend();
+          checkState("suspended");
+
+          await new Promise(resolve => setTimeout(resolve, TIMEOUT * 3));
+
+          checkState("suspended");
+          filter.disconnect();
+          checkState("disconnected");
+
+          for (let method of ["suspend", "resume", "close"]) {
+            browser.test.assertThrows(
+              () => {
+                filter[method]();
+              },
+              /.*/,
+              `(${num}): ${method}() should throw while disconnected`);
+          }
+
+          browser.test.assertThrows(
+            () => {
+              filter.write(encoder.encode("Foo bar"));
+            },
+            /.*/,
+            `(${num}): write() should throw while disconnected`);
+
+          filter.disconnect();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "slow_response.sjs",
+    task(filter, resolve, num) {
+      let encoder = new TextEncoder("utf-8");
+
+      filter.onstop = event => {
+        browser.test.fail(`(${num}): Got unexpected onStop event while disconnected`);
+      };
+
+      browser.test.assertThrows(
+        () => {
+          filter.write(encoder.encode("Foo bar"));
+        },
+        /.*/,
+        `(${num}): write() should throw prior to connection`);
+
+      let n = 0;
+      filter.ondata = async event => {
+        n++;
+
+        filter.write(event.data);
+
+        function checkState(state) {
+          browser.test.assertEq(state, filter.status, `(${num}): Got expected status`);
+        }
+        if (n == 3) {
+          filter.close();
+
+          checkState("closed");
+
+          for (let method of ["suspend", "resume", "disconnect"]) {
+            browser.test.assertThrows(
+              () => {
+                filter[method]();
+              },
+              /.*/,
+              `(${num}): ${method}() should throw while closed`);
+          }
+
+          browser.test.assertThrows(
+            () => {
+              filter.write(encoder.encode("Foo bar"));
+            },
+            /.*/,
+            `(${num}): write() should throw while disconnected`);
+
+          filter.close();
+
+          resolve();
+        }
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.slice(0, 3).join(""), "Got expected final HTML");
+    },
+  },
+  {
+    url: "lorem.html.gz",
+    task(filter, resolve, num) {
+      let response = "";
+      let decoder = new TextDecoder("utf-8");
+
+      filter.onstart = event => {
+        browser.test.log(`(${num}): Request start`);
+      };
+
+      filter.onstop = event => {
+        browser.test.assertEq("finishedtransferringdata", filter.status,
+                              `(${num}): Got expected onStop status`);
+
+        filter.close();
+        browser.test.assertEq("closed", filter.status,
+                              `Got expected closed status`);
+
+
+        browser.test.assertEq(JSON.stringify(PARTS.join("")),
+                              JSON.stringify(response),
+                              `(${num}): Got expected response`);
+
+        resolve();
+      };
+
+      filter.ondata = event => {
+        let str = decoder.decode(event.data, {stream: true});
+        response += str;
+
+        filter.write(event.data);
+      };
+
+      filter.onerror = event => {
+        browser.test.fail(`(${num}): Got unexpected error event: ${filter.error}`);
+      };
+    },
+    verify(response) {
+      is(response, PARTS.join(""), "Got expected final HTML");
+    },
+  },
+];
+
+function serializeTest(test, num) {
+  /* globals ExtensionTestCommon */
+
+  let url = `${test.url}?test_num=${num}`;
+  let task = ExtensionTestCommon.serializeFunction(test.task);
+
+  return `{url: ${JSON.stringify(url)}, task: ${task}}`;
+}
+
+add_task(async function() {
+  function background(TASKS) {
+    async function runTest(test, num, details) {
+      browser.test.log(`Running test #${num}: ${details.url}`);
+
+      let filter = browser.webRequest.filterResponseData(details.requestId);
+
+      try {
+        await new Promise(resolve => {
+          test.task(filter, resolve, num, details);
+        });
+      } catch (e) {
+        browser.test.fail(`Task #${num} threw an unexpected exception: ${e} :: ${e.stack}`);
+      }
+
+      browser.test.log(`Finished test #${num}: ${details.url}`);
+      browser.test.sendMessage(`finished-${num}`);
+    }
+
+    browser.webRequest.onBeforeRequest.addListener(
+      details => {
+        for (let [num, test] of TASKS.entries()) {
+          if (details.url.endsWith(test.url)) {
+            runTest(test, num, details);
+            break;
+          }
+        }
+      }, {
+        urls: ["http://mochi.test/*?test_num=*"],
+      },
+      ["blocking"]);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `
+      const PARTS = ${JSON.stringify(PARTS)};
+      const TIMEOUT = ${TIMEOUT};
+
+      (${background})([${TASKS.map(serializeTest)}])
+    `,
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+
+  async function runTest(test, num) {
+    let url = `${test.url}?test_num=${num}`;
+
+    let resp = await fetch(url);
+    let body = await resp.text();
+
+    await extension.awaitMessage(`finished-${num}`);
+
+    info(`Verifying test #${num}: ${url}`);
+    await test.verify(body);
+  }
+
+  if (SEQUENTIAL) {
+    for (let [num, test] of TASKS.entries()) {
+      await runTest(test, num);
+    }
+  } else {
+    await Promise.all(TASKS.map(runTest));
+  }
+
+  await extension.unload();
+});
+
+add_task(async function test_permissions() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      browser.test.assertEq(
+          undefined, browser.webRequest.filterResponseData,
+          "filterResponseData is undefined without blocking permissions");
+    },
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+  await extension.unload();
+});
+
+add_task(async function test_invalidId() {
+  let extension = ExtensionTestUtils.loadExtension({
+    async background() {
+      let filter = browser.webRequest.filterResponseData("34159628");
+
+      await new Promise(resolve => { filter.onerror = resolve; });
+
+      browser.test.assertEq("Invalid request ID",
+                            filter.error,
+                            "Got expected error");
+
+      browser.test.notifyPass("invalid-request-id");
+    },
+
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "http://mochi.test/",
+      ],
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("invalid-request-id");
+  await extension.unload();
+});
+</script>
+</body>
+</html>