Bug 1289319 - Add a test framework for the first party isolation tests. r?baku, arthuredelstein
--- 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);
+ }
+
+ });
+ }
+};