toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
author Luca Greco <lgreco@mozilla.com>
Thu, 14 Mar 2019 18:06:33 +0000
changeset 524921 32112b7c202edccb5ec54ab18971e0a46af3ee58
parent 512110 1fd1e34371da3e3ab587a2311528b4ac235d0b67
child 531524 c9c0c6f2eed54a187e124942e53c3660b4cf17d8
permissions -rw-r--r--
Bug 1516862 - Fix test_ext_webrequest_basic intermittency while testing onError events fired on hostname resolving error. r=mixedpuppy When the dns resolution for an url's hostname fails, the webRequest API seems to be receiving (from time to time but more often on macos and windows) more then one call to the HttpObserverManager.observeActivity method for the activityType nsISocketTransport.STATUS_RESOLVING and nsISocketTransport.STATUS_RESOLVED while channel wrapper errorString property is still null. The observeActvity method goal seems to fire fallback WebRequest errors for error conditions that are not fired as ChannelWrapper error events, but the "NS_ERROR_UNKNOWN_HOST" error is also being fired right after the fallback webRequest error event fired as "NS_ERROR_NET_ON_RESOLVED" (or "NS_ERROR_NET_ON_RESOLVING"). This behavior consistently triggers the two intermittent failures on the test test_ext_webrequest_basic.html while executing the test_webRequest_frames task "expected error message received in onErrorOccurred - Expected: NS_ERROR_UNKNOWN_HOST, Actual: NS_ERROR_NET_ON_RESOLVED" (when the fallback NS_ERROR_NET_ON_RESOLVED is fired first) and then "received onErrorOccurred" (because the "NS_ERROR_UNKNOWN_HOST" error is also being fired right after the fallback error message). Differential Revision: https://phabricator.services.mozilla.com/D22587

<!DOCTYPE HTML>

<html>
<head>
<meta charset="utf-8">
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
  <script type="text/javascript" src="head_webrequest.js"></script>
  <script type="text/javascript" src="head.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script>
"use strict";

function promiseWindowEvent(name, accept) {
  return new Promise(resolve => {
    window.addEventListener(name, function listener(event) {
      if (event.data !== accept) {
        return;
      }
      window.removeEventListener(name, listener);
      resolve(event);
    });
  });
}

if (AppConstants.platform === "android") {
  SimpleTest.requestLongerTimeout(3);
}

let extension;
add_task(async function setup() {
  // Clear the image cache, since it gets in the way otherwise.
  let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
  let cache = imgTools.getImgCacheForDocument(document);
  cache.clearCache(false);
  function clearCache() {
    ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services.cache2.clear();
  }
  SpecialPowers.loadChromeScript(clearCache);

  await SpecialPowers.pushPrefEnv({
    set: [["network.http.rcwn.enabled", false]],
  });

  extension = makeExtension();
  await extension.startup();
});

// expect is a set of test values used by the background script.
//
// type: type of request action
// events: optional, If defined only the events listed are expected for the
//                   request. If undefined, all events except onErrorOccurred
//                   and onBeforeRedirect are expected.  Must be in order received.
// redirect: url to redirect to during onBeforeSendHeaders
// status: number    expected status during onHeadersReceived, 200 default
// cancel: event in which we return cancel=true.  cancelled message is sent.
// cached: expected fromCache value, default is false, checked in onCompletion
// headers: request or response headers to modify
// origin: The expected originUrl, a default origin can be passed for all files

add_task(async function test_webRequest_links() {
  let expect = {
    "file_style_bad.css": {
      type: "stylesheet",
      events: ["onBeforeRequest", "onErrorOccurred"],
      cancel: "onBeforeRequest",
    },
    "file_style_redirect.css": {
      type: "stylesheet",
      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
      optional_events: ["onHeadersReceived"],
      redirect: "file_style_good.css",
    },
    "file_style_good.css": {
      type: "stylesheet",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addStylesheet("file_style_bad.css");
  await extension.awaitMessage("cancelled");
  // we redirect to style_good which completes the test
  addStylesheet("file_style_redirect.css");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_images() {
  let expect = {
    "file_image_bad.png": {
      type: "image",
      events: ["onBeforeRequest", "onErrorOccurred"],
      cancel: "onBeforeRequest",
    },
    "file_image_redirect.png": {
      type: "image",
      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
      optional_events: ["onHeadersReceived"],
      redirect: "file_image_good.png",
    },
    "file_image_good.png": {
      type: "image",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addImage("file_image_bad.png");
  await extension.awaitMessage("cancelled");
  // we redirect to image_good which completes the test
  addImage("file_image_redirect.png");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_scripts() {
  let expect = {
    "file_script_bad.js": {
      type: "script",
      events: ["onBeforeRequest", "onErrorOccurred"],
      cancel: "onBeforeRequest",
    },
    "file_script_redirect.js": {
      type: "script",
      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
      optional_events: ["onHeadersReceived"],
      redirect: "file_script_good.js",
    },
    "file_script_good.js": {
      type: "script",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  let message = promiseWindowEvent("message", "test1");
  addScript("file_script_bad.js");
  await extension.awaitMessage("cancelled");
  // we redirect to script_good which completes the test
  addScript("file_script_redirect.js?q=test1");
  await extension.awaitMessage("done");

  is((await message).data, "test1", "good script ran");
});

add_task(async function test_webRequest_xhr_get() {
  let expect = {
    "file_script_xhr.js": {
      type: "script",
    },
    "xhr_resource": {
      status: 404,
      type: "xmlhttprequest",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addScript("file_script_xhr.js");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_nonexistent() {
  let expect = {
    "nonexistent_script_url.js": {
      status: 404,
      type: "script",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addScript("nonexistent_script_url.js");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_checkCached() {
  let expect = {
    "file_image_good.png": {
      type: "image",
      cached: true,
    },
    "file_script_good.js": {
      type: "script",
      cached: true,
    },
    "file_style_good.css": {
      type: "stylesheet",
      cached: true,
    },
    "nonexistent_script_url.js": {
      status: 404,
      type: "script",
      cached: false,
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  let message = promiseWindowEvent("message", "test1");

  addImage("file_image_good.png");
  addScript("file_script_good.js?q=test1");

  is((await message).data, "test1", "good script ran");

  addStylesheet("file_style_good.css");
  addScript("nonexistent_script_url.js");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_headers() {
  let expect = {
    "file_script_nonexistent.js": {
      type: "script",
      status: 404,
      headers: {
        request: {
          add: {
            "X-WebRequest-request": "text",
            "X-WebRequest-request-binary": "binary",
          },
          modify: {
            "user-agent": "WebRequest",
          },
          remove: [
            "referer",
          ],
        },
        response: {
          add: {
            "X-WebRequest-response": "text",
            "X-WebRequest-response-binary": "binary",
          },
          modify: {
            "server": "WebRequest",
            "content-type": "text/html; charset=utf-8",
          },
          remove: [
            "connection",
          ],
        },
      },
      completion: "onCompleted",
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addScript("file_script_nonexistent.js");
  await extension.awaitMessage("done");
});

add_task(async function test_webRequest_tabId() {
  function background() {
    let tab;
    browser.tabs.onCreated.addListener(newTab => {
      tab = newTab;
    });

    browser.test.onMessage.addListener(msg => {
      if (msg === "close-tab") {
        browser.tabs.remove(tab.id);
        browser.test.sendMessage("tab-closed");
      }
    });
  }

  let tabExt = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: [
        "tabs",
      ],
    },
    background,
  });
  await tabExt.startup();

  let linkUrl = `file_WebRequest_page3.html?trigger=a&nocache=${Math.random()}`;
  let expect = {
    "file_WebRequest_page3.html": {
      type: "main_frame",
    },
  };

  if (AppConstants.platform != "android") {
    expect["favicon.ico"] = {
      type: "image",
      origin: SimpleTest.getTestFileURL(linkUrl),
      cached: false,
    };
  }

  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  let a = addLink(linkUrl);
  a.click();
  await extension.awaitMessage("done");

  let closed = tabExt.awaitMessage("tab-closed");
  tabExt.sendMessage("close-tab");
  await closed;

  await tabExt.unload();
});

add_task(async function test_webRequest_tabId_browser() {
  async function background(url) {
    let tabId;
    browser.test.onMessage.addListener(async (msg, expected) => {
      if (msg == "create") {
        let tab = await browser.tabs.create({url});
        tabId = tab.id;
        return;
      }
      if (msg == "done") {
        await browser.tabs.remove(tabId);
        browser.test.sendMessage("done");
      }
    });
    browser.test.sendMessage("origin", browser.runtime.getURL("/"));
  }

  let pageUrl = `${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}`;
  let tabExt = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: [
        "tabs",
      ],
    },
    background: `(${background})('${pageUrl}')`,
  });

  let expect = {
    "file_sample.html": {
      type: "main_frame",
    },
  };

  if (AppConstants.platform != "android") {
    expect["favicon.ico"] = {
      type: "image",
      origin: pageUrl,
      cached: true,
    };
  }

  await tabExt.startup();
  let origin = await tabExt.awaitMessage("origin");

  // expecting origin == extension baseUrl
  extension.sendMessage("set-expected", {expect, origin});
  await extension.awaitMessage("continue");

  // open a tab from an extension principal
  tabExt.sendMessage("create");
  await extension.awaitMessage("done");
  tabExt.sendMessage("done");
  await tabExt.awaitMessage("done");
  await tabExt.unload();
});

add_task(async function test_webRequest_frames() {
  let expect = {
    "text/plain,webRequestTest": {
      type: "sub_frame",
      events: ["onBeforeRequest", "onCompleted"],
    },
    "text/plain,webRequestTest_bad": {
      type: "sub_frame",
      events: ["onBeforeRequest", "onCompleted"],
      cancel: "onBeforeRequest",
    },
    "redirection.sjs": {
      status: 302,
      type: "sub_frame",
      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"],
    },
    "dummy_page.html": {
      type: "sub_frame",
      status: 404,
    },
    "badrobot": {
      type: "sub_frame",
      status: 404,
      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"],
      // When an url's hostname fails to be resolved, an NS_ERROR_NET_ON_RESOLVED/RESOLVING
      // onError event may be fired right before the NS_ERROR_UNKNOWN_HOST
      // (See Bug 1516862 for a rationale).
      optional_events: ["onErrorOccurred"],
      error: ["NS_ERROR_UNKNOWN_HOST", "NS_ERROR_NET_ON_RESOLVED", "NS_ERROR_NET_ON_RESOLVING"],
    },
  };
  extension.sendMessage("set-expected", {expect, origin: location.href});
  await extension.awaitMessage("continue");
  addFrame("data:text/plain,webRequestTest");
  addFrame("data:text/plain,webRequestTest_bad");
  await extension.awaitMessage("cancelled");
  addFrame("redirection.sjs");
  addFrame("https://nonresolvablehostname.invalid/badrobot");
  await extension.awaitMessage("done");
});

add_task(async function teardown() {
  await extension.unload();
});

add_task(async function test_case_preserving() {
  const manifest = {
    permissions: [
      "webRequest",
      "webRequestBlocking",
      "http://mochi.test/",
    ],
  };

  async function background() {
    // This is testing if header names preserve case,
    // so the case-sensitive comparison is on purpose.
    function ua({url, requestHeaders}) {
      if (url.endsWith("?blind-add")) {
        requestHeaders.push({name: "user-agent", value: "Blind/Add"});
        return {requestHeaders};
      }
      for (const header of requestHeaders) {
        if (header.name === "User-Agent") {
          header.value = "Case/Sensitive";
        }
      }
      return {requestHeaders};
    }

    await browser.webRequest.onBeforeSendHeaders.addListener(ua, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
    browser.test.sendMessage("ready");
  }

  const extension = ExtensionTestUtils.loadExtension({manifest, background});

  await extension.startup();
  await extension.awaitMessage("ready");

  const response1 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs"));
  const headers1 = JSON.parse(await response1.text());

  is(headers1["user-agent"], "Case/Sensitive", "User-Agent header matched and changed.");

  const response2 = await fetch(SimpleTest.getTestFileURL("return_headers.sjs?blind-add"));
  const headers2 = JSON.parse(await response2.text());

  is(headers2["user-agent"], "Blind/Add", "User-Agent header blindly added.");

  await extension.unload();
});

</script>
</head>
<body>
<div id="test">Sample text</div>

</body>
</html>