Bug 1289319 - Add a test framework for the first party isolation tests. r?baku, arthuredelstein draft
authorTim Huang <tihuang@mozilla.com>
Mon, 22 Aug 2016 22:05:01 +0800
changeset 404345 ccf700ed138757d9032a223b13c0e6efdf4672bd
parent 403581 f97a056ae6235de7855fd8aaa04fb1c8d183bd06
child 529162 b06b9233945bb312385afaa821a44819a16d6a45
push id27187
push userbmo:tihuang@mozilla.com
push dateTue, 23 Aug 2016 08:58:11 +0000
reviewersbaku, arthuredelstein
bugs1289319
milestone51.0a1
Bug 1289319 - Add a test framework for the first party isolation tests. r?baku, arthuredelstein
browser/components/moz.build
browser/components/originattributes/moz.build
browser/components/originattributes/test/browser/.eslintrc
browser/components/originattributes/test/browser/browser.ini
browser/components/originattributes/test/browser/browser_dummy.js
browser/components/originattributes/test/browser/file_firstPartyBasic.html
browser/components/originattributes/test/browser/head.js
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'contextualidentity',
     'customizableui',
     'dirprovider',
     'downloads',
     'extensions',
     'feeds',
     'migration',
     'newtab',
+    'originattributes',
     'places',
     'preferences',
     'privatebrowsing',
     'search',
     'sessionstore',
     'shell',
     'selfsupport',
     'syncedtabs',
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser/browser.ini',
+]
+
+with Files('**'):
+    BUG_COMPONENT = ('Firefox', 'OriginAttributes')
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/.eslintrc
@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc"
+  ]
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+skip-if = buildapp == "mulet"
+tags = usercontextid firstpartyisolation originattributes
+support-files =
+  file_firstPartyBasic.html
+  head.js
+
+[browser_dummy.js]
+skip-if = true
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/browser_dummy.js
@@ -0,0 +1,10 @@
+/*
+ * This is a dummy test case which makes this could be built.
+ * Should be removed after actual tests landed.
+ */
+
+"use strict";
+
+add_task(function* () {
+  ok(true, "Make this test pass anyway.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/file_firstPartyBasic.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>First Party Isolation Tests</title>
+  </head>
+  <body>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/head.js
@@ -0,0 +1,334 @@
+/* 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/. */
+
+'use strict';
+
+const TEST_URL_PATH = "/browser/browser/components/originattributes/test/browser/";
+
+// The flags of test modes.
+const TEST_MODE_FIRSTPARTY   = 0;
+const TEST_MODE_NO_ISOLATION = 1;
+const TEST_MODE_CONTAINERS   = 2;
+
+// The name of each mode.
+const TEST_MODE_NAMES = [ "first party isolation",
+                          "no isolation",
+                          "containers" ];
+
+// The frame types.
+const TEST_TYPE_FRAME  = 0;
+const TEST_TYPE_IFRAME = 1;
+
+// The default frame setting.
+const DEFAULT_FRAME_SETTING = [ TEST_TYPE_IFRAME ];
+
+let gFirstPartyBasicPage = TEST_URL_PATH + "file_firstPartyBasic.html";
+
+/**
+ * Add a tab for the given url with the specific user context id.
+ *
+ * @param aURL
+ *    The url of the page.
+ * @param aUserContextId
+ *    The user context id for this tab.
+ *
+ * @return tab     - The tab object of this tab.
+ *         browser - The browser object of this tab.
+ */
+function* openTabInUserContext(aURL, aUserContextId) {
+  // Open the tab in the correct userContextId.
+  let tab = gBrowser.addTab(aURL, {userContextId: aUserContextId});
+
+  // Select tab and make sure its browser is focused.
+  gBrowser.selectedTab = tab;
+  tab.ownerGlobal.focus();
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  yield BrowserTestUtils.browserLoaded(browser);
+  return {tab, browser};
+}
+
+/**
+ * Add a tab for a page with the given first party domain. This page will have
+ * an iframe which is loaded with the given url by default or you could specify
+ * a frame setting to create nested frames. And this function will also modify
+ * the 'content' in the ContentTask to the target frame's window object.
+ *
+ * @param aURL
+ *    The url of the iframe.
+ * @param aFirstPartyDomain
+ *    The first party domain.
+ * @param aFrameSetting
+ *    This setting controls how frames are organized within the page. The
+ *    setting is an array of frame types, the first item indicates the
+ *    frame type (iframe or frame) of the first layer of the frame structure,
+ *    and the second item indicates the second layer, and so on. The aURL will
+ *    be loaded at the deepest layer. This is optional.
+ *
+ * @return tab     - The tab object of this tab.
+ *         browser - The browser object of this tab.
+ */
+function* openTabInFirstParty(aURL, aFirstPartyDomain,
+                              aFrameSetting = DEFAULT_FRAME_SETTING) {
+
+  // If the first party domain ends with '/', we remove it.
+  if (aFirstPartyDomain.endsWith('/')) {
+    aFirstPartyDomain = aFirstPartyDomain.slice(0, -1);
+  }
+
+  let basicPageURL = aFirstPartyDomain + gFirstPartyBasicPage;
+
+  // Open the tab for the basic first party page.
+  let tab = gBrowser.addTab(basicPageURL);
+
+  // Select tab and make sure its browser is focused.
+  gBrowser.selectedTab = tab;
+  tab.ownerGlobal.focus();
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  yield BrowserTestUtils.browserLoaded(browser);
+
+  let pageArgs = { url: aURL,
+                   frames: aFrameSetting,
+                   typeFrame: TEST_TYPE_FRAME,
+                   typeIFrame: TEST_TYPE_IFRAME,
+                   basicFrameSrc: basicPageURL};
+
+  // Create the frame structure.
+  yield ContentTask.spawn(browser, pageArgs, function* (arg) {
+    let typeFrame = arg.typeFrame;
+    let typeIFrame = arg.typeIFrame;
+
+    // Redefine the 'content' for allowing us to change its target, and making
+    // ContentTask.spawn can directly work on the frame element.
+    this.frameWindow = content;
+
+    Object.defineProperty(this, "content", {
+      get: () => this.frameWindow
+    });
+
+    let frameElement;
+    let numOfLayers = 0;
+
+    for (let type of arg.frames) {
+      let document = content.document;
+      numOfLayers++;
+
+      if (type === typeFrame) {
+        // Add a frameset which carries the frame element.
+        let frameSet = document.createElement('frameset');
+        frameSet.cols = "50%,50%";
+
+        let frame = document.createElement('frame');
+        let dummyFrame = document.createElement('frame');
+
+        frameSet.appendChild(frame);
+        frameSet.appendChild(dummyFrame);
+
+        document.body.appendChild(frameSet);
+
+        frameElement = frame;
+      } else if (type === typeIFrame) {
+        // Add an iframe.
+        let iframe = document.createElement('iframe');
+        document.body.appendChild(iframe);
+
+        frameElement = iframe;
+      } else {
+        ok(false, "Invalid frame type.");
+        break;
+      }
+
+      // Wait for the frame to be loaded.
+      yield new Promise(done => {
+        frameElement.addEventListener("load", function loadEnd() {
+          frameElement.removeEventListener("load", loadEnd, true);
+          done();
+        }, true);
+
+        // If it is the deepest layer, we load the target URL. Otherwise, we
+        // load a basic page.
+        if (numOfLayers === arg.frames.length) {
+          frameElement.setAttribute("src", arg.url);
+        } else {
+          frameElement.setAttribute("src", arg.basicFrameSrc);
+        }
+      });
+
+      // Redirect the 'content' to the frame's window.
+      this.frameWindow = frameElement.contentWindow;
+    }
+  });
+
+  return {tab, browser};
+}
+
+this.IsolationTestTools = {
+  /**
+   * Adds isolation tests for first party isolation, no isolation
+   * and containers respectively.
+   *
+   * @param aTask
+   *    The testing task which will be run in different settings.
+   */
+  _add_task(aTask) {
+    add_task(function* addTaskForIsolationTests() {
+      let testSettings = [
+        { mode: TEST_MODE_FIRSTPARTY,
+          skip: false,
+          prefs: [["privacy.firstparty.isolate", true]]
+        },
+        { mode: TEST_MODE_NO_ISOLATION,
+          skip: false,
+          prefs: [["privacy.firstparty.isolate", false]]
+        },
+        { mode: TEST_MODE_CONTAINERS,
+          skip: false,
+          prefs: [["privacy.userContext.enabled", true]]
+        },
+      ];
+
+      // Add test tasks.
+      for (let testSetting of testSettings) {
+        IsolationTestTools._addTaskForMode(testSetting.mode,
+                                           testSetting.prefs,
+                                           testSetting.skip,
+                                           aTask);
+      }
+    });
+  },
+
+  _addTaskForMode(aMode, aPref, aSkip, aTask) {
+    if (aSkip) {
+      return;
+    }
+
+    add_task(function* () {
+      info("Starting the test for " + TEST_MODE_NAMES[aMode]);
+
+      // Before run this task, reset the preferences first.
+      yield SpecialPowers.flushPrefEnv();
+
+      // Make sure preferences are set properly.
+      yield SpecialPowers.pushPrefEnv({"set": aPref});
+
+      yield aTask(aMode);
+    });
+  },
+
+  /**
+   * Add a tab with the given tab setting, this will open different types of
+   * tabs according to the given test mode. A tab setting means a isolation
+   * target in different test mode; a tab setting indicates a first party
+   * domain when testing the first party isolation, it is a user context
+   * id when testing containers.
+   *
+   * @param aMode
+   *    The test mode which decides what type of tabs will be opened.
+   * @param aURL
+   *    The url which is going to open.
+   * @param aTabSettingObj
+   *    The tab setting object includes 'firstPartyDomain' for the first party
+   *    domain and 'userContextId' for Containers.
+   * @param aFrameSetting
+   *    This setting controls how frames are organized within the page. The
+   *    setting is an array of frame types, the first item indicates the
+   *    frame type (iframe or frame) of the first layer of the frame structure,
+   *    and the second item indicates the second layer, and so on. The aURL
+   *    will be loaded at the deepest layer. This is optional.
+   *
+   * @return tab     - The tab object of this tab.
+   *         browser - The browser object of this tab.
+   */
+  _addTab(aMode, aURL, aTabSettingObj, aFrameSetting) {
+    if (aMode === TEST_MODE_CONTAINERS) {
+      return openTabInUserContext(aURL, aTabSettingObj.userContextId);
+    }
+
+    return openTabInFirstParty(aURL, aTabSettingObj.firstPartyDomain,
+                                aFrameSetting);
+
+  },
+
+  /**
+   * Run isolation tests. The framework will run tests with standard combinations
+   * of prefs and tab settings, and checks whether the isolation is working.
+   *
+   * @param aURL
+   *    The URL of the page that will be tested or an object contains 'url',
+   *    the tested page, 'firstFrameSetting' for the frame setting of the first
+   *    tab, and 'secondFrameSetting' for the second tab.
+   * @param aGetResultFunc
+   *    A function which is responsible for returning the isolation result back
+   *    to the framework for further checking. This function will be provided
+   *    the browser object of the tab, that allows modifying or fetching results
+   *    from the page content.
+   * @param aCompareResultFunc
+   *    An optional function which allows modifying the way how does framework
+   *    check results. This function will be provided a boolean to indicate
+   *    the isolation is no or off and two results. This function should return
+   *    a boolean to tell that whether isolation is working. If this function
+   *    is not given, the framework will take case checking by itself.
+   */
+  runTests(aURL, aGetResultFunc, aCompareResultFunc) {
+    let pageURL;
+    let firstFrameSetting;
+    let secondFrameSetting;
+
+    if (typeof aURL === "string") {
+      pageURL = aURL;
+    } else if (typeof aURL === "object") {
+      pageURL = aURL.url;
+      firstFrameSetting = aURL.firstFrameSetting;
+      secondFrameSetting = aURL.secondFrameSetting;
+    }
+
+    let tabSettings = [
+                        { firstPartyDomain: "http://example.com", userContextId: 1},
+                        { firstPartyDomain: "http://example.org", userContextId: 2}
+                      ];
+
+    this._add_task(function* (aMode) {
+      let tabSettingA = 0;
+
+      for (let tabSettingB of [0, 1]) {
+        // Create Tabs.
+        let tabInfoA = yield IsolationTestTools._addTab(aMode,
+                                                        pageURL,
+                                                        tabSettings[tabSettingA],
+                                                        firstFrameSetting);
+        let tabInfoB = yield IsolationTestTools._addTab(aMode,
+                                                        pageURL,
+                                                        tabSettings[tabSettingB],
+                                                        secondFrameSetting);
+
+        // Fetch results from tabs.
+        let resultA = yield aGetResultFunc(tabInfoA.browser);
+        let resultB = yield aGetResultFunc(tabInfoB.browser);
+
+        // Close Tabs.
+        yield BrowserTestUtils.removeTab(tabInfoA.tab);
+        yield BrowserTestUtils.removeTab(tabInfoB.tab);
+
+        // Compare results.
+        let result = false;
+        let shouldIsolate = (aMode !== TEST_MODE_NO_ISOLATION) &&
+                            tabSettingA !== tabSettingB;
+        if (aCompareResultFunc) {
+          result = yield aCompareResultFunc(shouldIsolate, resultA, resultB);
+        } else {
+          result = shouldIsolate ? resultA !== resultB :
+                                   resultA === resultB;
+        }
+
+        let msg = `Testing ${TEST_MODE_NAMES[aMode]} for ` +
+                  `isolation ${shouldIsolate ? "on" : "off"} with TabSettingA ` +
+                  `${tabSettingA} and tabSettingB ${tabSettingB}`;
+
+        ok(result, msg);
+      }
+
+    });
+  }
+};