diff options
Diffstat (limited to '')
8 files changed, 1109 insertions, 0 deletions
diff --git a/browser/base/content/test/captivePortal/browser.toml b/browser/base/content/test/captivePortal/browser.toml new file mode 100644 index 0000000000..bdf17daf9c --- /dev/null +++ b/browser/base/content/test/captivePortal/browser.toml @@ -0,0 +1,16 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_CaptivePortalWatcher.js"] +skip-if = ["os == 'win'"] # Bug 1313894 + +["browser_CaptivePortalWatcher_1.js"] +skip-if = ["os == 'win'"] # Bug 1313894 + +["browser_captivePortalTabReference.js"] + +["browser_captivePortal_certErrorUI.js"] + +["browser_captivePortal_https_only.js"] + +["browser_closeCapPortalTabCanonicalURL.js"] diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js new file mode 100644 index 0000000000..aaccc77152 --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js @@ -0,0 +1,125 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +// Bug 1318389 - This test does a lot of window and tab manipulation, +// causing it to take a long time on debug. +requestLongerTimeout(2); + +add_task(setupPrefsAndRecentWindowBehavior); + +// Each of the test cases below is run twice: once for login-success and once +// for login-abort (aSuccess set to true and false respectively). +let testCasesForBothSuccessAndAbort = [ + /** + * A portal is detected when there's no browser window, then a browser + * window is opened, then the portal is freed. + * The portal tab should be added and focused when the window is + * opened, and closed automatically when the success event is fired. + * The captive portal notification should be shown when the window is + * opened, and closed automatically when the success event is fired. + */ + async function test_detectedWithNoBrowserWindow_Open(aSuccess) { + await portalDetected(); + let win = await focusWindowAndWaitForPortalUI(); + await freePortal(aSuccess); + ensureNoPortalTab(win); + ensureNoPortalNotification(win); + await closeWindowAndWaitForWindowActivate(win); + }, + + /** + * A portal is detected when multiple browser windows are open but none + * have focus. A browser window is focused, then the portal is freed. + * The portal tab should be added and focused when the window is + * focused, and closed automatically when the success event is fired. + * The captive portal notification should be shown in all windows upon + * detection, and closed automatically when the success event is fired. + */ + async function test_detectedWithNoBrowserWindow_Focused(aSuccess) { + let win1 = await openWindowAndWaitForFocus(); + let win2 = await openWindowAndWaitForFocus(); + // Defocus both windows. + await SimpleTest.promiseFocus(window); + + await portalDetected(); + + // Notification should be shown in both windows. + await ensurePortalNotification(win1); + ensureNoPortalTab(win1); + await ensurePortalNotification(win2); + ensureNoPortalTab(win2); + + await focusWindowAndWaitForPortalUI(false, win2); + + await freePortal(aSuccess); + + ensureNoPortalNotification(win1); + ensureNoPortalTab(win2); + ensureNoPortalNotification(win2); + + await closeWindowAndWaitForWindowActivate(win2); + // No need to wait for xul-window-visible: after win2 is closed, focus + // is restored to the default window and win1 remains in the background. + await BrowserTestUtils.closeWindow(win1); + }, + + /** + * A portal is detected when there's no browser window, then a browser + * window is opened, then the portal is freed. + * The recheck triggered when the browser window is opened takes a + * long time. No portal tab should be added. + * The captive portal notification should be shown when the window is + * opened, and closed automatically when the success event is fired. + */ + async function test_detectedWithNoBrowserWindow_LongRecheck(aSuccess) { + await portalDetected(); + let win = await focusWindowAndWaitForPortalUI(true); + await freePortal(aSuccess); + ensureNoPortalTab(win); + ensureNoPortalNotification(win); + await closeWindowAndWaitForWindowActivate(win); + }, + + /** + * A portal is detected when there's no browser window, and the + * portal is freed before a browser window is opened. No portal + * UI should be shown when a browser window is opened. + */ + async function test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) { + await portalDetected(); + await freePortal(aSuccess); + let win = await openWindowAndWaitForFocus(); + // Wait for a while to make sure no UI is shown. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + ensureNoPortalTab(win); + ensureNoPortalNotification(win); + await closeWindowAndWaitForWindowActivate(win); + }, + + /** + * A portal is detected when a browser window has focus. No portal tab should + * be opened. A notification bar should be displayed in all browser windows. + */ + async function test_detectedWithFocus(aSuccess) { + let win1 = await openWindowAndWaitForFocus(); + let win2 = await openWindowAndWaitForFocus(); + await portalDetected(); + ensureNoPortalTab(win1); + ensureNoPortalTab(win2); + await ensurePortalNotification(win1); + await ensurePortalNotification(win2); + await freePortal(aSuccess); + ensureNoPortalNotification(win1); + ensureNoPortalNotification(win2); + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win1); + await waitForBrowserWindowActive(window); + }, +]; + +for (let testcase of testCasesForBothSuccessAndAbort) { + add_task(testcase.bind(null, true)); + add_task(testcase.bind(null, false)); +} diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js new file mode 100644 index 0000000000..3d9cdb8616 --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js @@ -0,0 +1,108 @@ +"use strict"; + +add_task(setupPrefsAndRecentWindowBehavior); + +let testcases = [ + /** + * A portal is detected when there's no browser window, + * then a browser window is opened, and the portal is logged into + * and redirects to a different page. The portal tab should be added + * and focused when the window is opened, and left open after login + * since it redirected. + */ + async function test_detectedWithNoBrowserWindow_Redirect() { + await portalDetected(); + let win = await focusWindowAndWaitForPortalUI(); + let browser = win.gBrowser.selectedTab.linkedBrowser; + let loadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + CANONICAL_URL_REDIRECTED + ); + BrowserTestUtils.startLoadingURIString(browser, CANONICAL_URL_REDIRECTED); + await loadPromise; + await freePortal(true); + ensurePortalTab(win); + ensureNoPortalNotification(win); + await closeWindowAndWaitForWindowActivate(win); + }, + + /** + * Test the various expected behaviors of the "Show Login Page" button + * in the captive portal notification. The button should be visible for + * all tabs except the captive portal tab, and when clicked, should + * ensure a captive portal tab is open and select it. + */ + async function test_showLoginPageButton() { + let win = await openWindowAndWaitForFocus(); + await portalDetected(); + let notification = await ensurePortalNotification(win); + await testShowLoginPageButtonVisibility(notification, "visible"); + + async function testPortalTabSelectedAndButtonNotVisible() { + is( + win.gBrowser.selectedTab, + tab, + "The captive portal tab should be selected." + ); + await testShowLoginPageButtonVisibility(notification, "hidden"); + } + + let button = notification.buttonContainer.querySelector( + "button.notification-button" + ); + async function clickButtonAndExpectNewPortalTab() { + let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL); + button.click(); + let tab = await p; + is( + win.gBrowser.selectedTab, + tab, + "The captive portal tab should be selected." + ); + return tab; + } + + // Simulate clicking the button. The portal tab should be opened and + // selected and the button should hide. + let tab = await clickButtonAndExpectNewPortalTab(); + await testPortalTabSelectedAndButtonNotVisible(); + + // Close the tab. The button should become visible. + BrowserTestUtils.removeTab(tab); + ensureNoPortalTab(win); + await testShowLoginPageButtonVisibility(notification, "visible"); + + // When the button is clicked, a new portal tab should be opened and + // selected. + tab = await clickButtonAndExpectNewPortalTab(); + + // Open another arbitrary tab. The button should become visible. When it's clicked, + // the portal tab should be selected. + let anotherTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await testShowLoginPageButtonVisibility(notification, "visible"); + button.click(); + is( + win.gBrowser.selectedTab, + tab, + "The captive portal tab should be selected." + ); + + // Close the portal tab and select the arbitrary tab. The button should become + // visible and when it's clicked, a new portal tab should be opened. + BrowserTestUtils.removeTab(tab); + win.gBrowser.selectedTab = anotherTab; + await testShowLoginPageButtonVisibility(notification, "visible"); + tab = await clickButtonAndExpectNewPortalTab(); + + BrowserTestUtils.removeTab(anotherTab); + await freePortal(true); + ensureNoPortalTab(win); + ensureNoPortalNotification(win); + await closeWindowAndWaitForWindowActivate(win); + }, +]; + +for (let testcase of testcases) { + add_task(testcase); +} diff --git a/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js new file mode 100644 index 0000000000..0108855a8e --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_captivePortalTabReference.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService +); + +async function checkCaptivePortalTabReference(evt, currState) { + await portalDetected(); + let errorTab = await openCaptivePortalErrorTab(); + let portalTab = await openCaptivePortalLoginTab(errorTab); + + // Release the reference held to the portal tab by sending success/abort events. + Services.obs.notifyObservers(null, evt); + await TestUtils.waitForCondition( + () => CPS.state == currState, + "Captive portal has been released" + ); + gBrowser.removeTab(errorTab); + + await portalDetected(); + Assert.equal(CPS.state, CPS.LOCKED_PORTAL, "Captive portal is locked again"); + errorTab = await openCaptivePortalErrorTab(); + let portalTab2 = await openCaptivePortalLoginTab(errorTab); + Assert.notEqual( + portalTab, + portalTab2, + "waitForNewTab in openCaptivePortalLoginTab should not have completed at this point if references were held to the old captive portal tab after login/abort." + ); + gBrowser.removeTab(portalTab); + gBrowser.removeTab(portalTab2); + + let errorTabReloaded = BrowserTestUtils.waitForErrorPage( + errorTab.linkedBrowser + ); + Services.obs.notifyObservers(null, "captive-portal-login-success"); + await errorTabReloaded; + + gBrowser.removeTab(errorTab); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["captivedetect.canonicalURL", CANONICAL_URL], + ["captivedetect.canonicalContent", CANONICAL_CONTENT], + ], + }); +}); + +let capPortalStates = [ + { + evt: "captive-portal-login-success", + state: CPS.UNLOCKED_PORTAL, + }, + { + evt: "captive-portal-login-abort", + state: CPS.UNKNOWN, + }, +]; + +for (let elem of capPortalStates) { + add_task(checkCaptivePortalTabReference.bind(null, elem.evt, elem.state)); +} diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js new file mode 100644 index 0000000000..6389338a6f --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["captivedetect.canonicalURL", CANONICAL_URL], + ["captivedetect.canonicalContent", CANONICAL_CONTENT], + ], + }); +}); + +// This tests the alternate cert error UI when we are behind a captive portal. +add_task(async function checkCaptivePortalCertErrorUI() { + info( + "Checking that the alternate cert error UI is shown when we are behind a captive portal" + ); + + // Open a second window in the background. Later, we'll check that + // when we click the button to open the captive portal tab, the tab + // only opens in the active window and not in the background one. + let secondWindow = await openWindowAndWaitForFocus(); + await SimpleTest.promiseFocus(window); + + await portalDetected(); + + // Check that we didn't open anything in the background window. + ensureNoPortalTab(secondWindow); + + let tab = await openCaptivePortalErrorTab(); + let browser = tab.linkedBrowser; + let portalTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + CANONICAL_URL + ); + + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + let loginButton = doc.getElementById("openPortalLoginPageButton"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.isVisible(loginButton), + "Captive portal error page UI is visible" + ); + + if (!Services.focus.focusedElement == loginButton) { + await ContentTaskUtils.waitForEvent(loginButton, "focus"); + } + + Assert.ok(true, "openPortalLoginPageButton has focus"); + info("Clicking the Open Login Page button"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + let portalTab = await portalTabPromise; + is( + gBrowser.selectedTab, + portalTab, + "Login page should be open in a new foreground tab." + ); + + // Check that we didn't open anything in the background window. + ensureNoPortalTab(secondWindow); + + // Make sure clicking the "Open Login Page" button again focuses the existing portal tab. + await BrowserTestUtils.switchTab(gBrowser, tab); + // Passing an empty function to BrowserTestUtils.switchTab lets us wait for an arbitrary + // tab switch. + portalTabPromise = BrowserTestUtils.switchTab(gBrowser, () => {}); + await SpecialPowers.spawn(browser, [], async () => { + info("Clicking the Open Login Page button."); + let loginButton = content.document.getElementById( + "openPortalLoginPageButton" + ); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + info("Opening captive portal login page"); + let portalTab2 = await portalTabPromise; + is(portalTab2, portalTab, "The existing portal tab should be focused."); + + // Check that we didn't open anything in the background window. + ensureNoPortalTab(secondWindow); + + let portalTabClosing = BrowserTestUtils.waitForTabClosing(portalTab); + let errorTabReloaded = BrowserTestUtils.waitForErrorPage(browser); + + Services.obs.notifyObservers(null, "captive-portal-login-success"); + await portalTabClosing; + + info( + "Waiting for error tab to be reloaded after the captive portal was freed." + ); + await errorTabReloaded; + await SpecialPowers.spawn(browser, [], () => { + let doc = content.document; + ok( + !doc.body.classList.contains("captiveportal"), + "Captive portal error page UI is not visible." + ); + }); + + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(secondWindow); +}); + +add_task(async function testCaptivePortalAdvancedPanel() { + info( + "Checking that the advanced section of the about:certerror UI is shown when we are behind a captive portal." + ); + await portalDetected(); + let tab = await openCaptivePortalErrorTab(); + let browser = tab.linkedBrowser; + + const waitForLocationChange = (async () => { + await BrowserTestUtils.waitForLocationChange(gBrowser, BAD_CERT_PAGE); + info("(waitForLocationChange resolved)"); + })(); + await SpecialPowers.spawn(browser, [BAD_CERT_PAGE], async expectedURL => { + const doc = content.document; + let advancedButton = doc.getElementById("advancedButton"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.isVisible(advancedButton), + "Captive portal UI is visible" + ); + + info("Clicking on the advanced button"); + const advPanel = doc.getElementById("badCertAdvancedPanel"); + ok( + !ContentTaskUtils.isVisible(advPanel), + "Advanced panel is not yet visible" + ); + await EventUtils.synthesizeMouseAtCenter(advancedButton, {}, content); + ok(ContentTaskUtils.isVisible(advPanel), "Advanced panel is now visible"); + + let advPanelContent = doc.getElementById("badCertTechnicalInfo"); + ok( + ContentTaskUtils.isVisible(advPanelContent) && + advPanelContent.textContent.includes("expired.example.com"), + "Advanced panel text content is visible" + ); + + let advPanelErrorCode = doc.getElementById("errorCode"); + ok( + advPanelErrorCode.textContent, + "Cert error code is visible in the advanced panel" + ); + + // - + + const advPanelExceptionButton = doc.getElementById("exceptionDialogButton"); + + function isOnCertErrorPage() { + return ContentTaskUtils.isVisible(advPanel); + } + + ok(isOnCertErrorPage(), "On cert error page before adding exception"); + ok( + advPanelExceptionButton.disabled, + "Exception button should start disabled" + ); + await EventUtils.synthesizeMouseAtCenter( + advPanelExceptionButton, + {}, + content + ); // Click + const clickTime = content.performance.now(); + ok( + isOnCertErrorPage(), + "Still on cert error page because clicked too early" + ); + + // Now waitForCondition now that it's possible. + try { + await ContentTaskUtils.waitForCondition( + () => !advPanelExceptionButton.disabled, + "Wait for exception button enabled" + ); + } catch (rejected) { + ok(false, rejected); + return; + } + ok( + !advPanelExceptionButton.disabled, + "Exception button should be enabled after waiting" + ); + const msSinceClick = content.performance.now() - clickTime; + const expr = `${msSinceClick} > 1000`; + /* eslint-disable no-eval */ + ok(eval(expr), `Exception button should stay disabled for ${expr} ms`); + + await EventUtils.synthesizeMouseAtCenter( + advPanelExceptionButton, + {}, + content + ); // Click + info("Clicked"); + }); + await waitForLocationChange; + info("Page reloaded after adding cert exception"); + + // Clear the certificate exception. + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.clearValidityOverride("expired.example.com", -1, {}); + + info("After clearing cert override, asking for reload..."); + const waitForErrorPage = BrowserTestUtils.waitForErrorPage(browser); + await SpecialPowers.spawn(browser, [], async () => { + info("reload..."); + content.location.reload(); + }); + info("waitForErrorPage..."); + await waitForErrorPage; + + info("removeTab..."); + await BrowserTestUtils.removeTab(tab); + info("Done!"); +}); diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js b/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js new file mode 100644 index 0000000000..9b89484ef1 --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_captivePortal_https_only.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); +const CANONICAL_URI = Services.io.newURI(testPath); +const PERMISSION_NAME = "https-only-load-insecure"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + // That changes the canoncicalURL from "http://{server}/captive-detect/success.txt" + // to http://example.com + set: [ + ["captivedetect.canonicalURL", testPath], + ["dom.security.https_only_mode", true], + ], + }); +}); + +// This test checks if https-only exempts the canoncial uri. +add_task(async function checkCaptivePortalExempt() { + await portalDetected(); + info("Checking that the canonical uri is exempt by https-only mode"); + let tab = await openCaptivePortalErrorTab(); + let browser = tab.linkedBrowser; + let portalTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, testPath); + + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + let loginButton = doc.getElementById("openPortalLoginPageButton"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.isVisible(loginButton), + "Captive portal error page UI is visible" + ); + + if (!Services.focus.focusedElement == loginButton) { + await ContentTaskUtils.waitForEvent(loginButton, "focus"); + } + + Assert.ok(true, "openPortalLoginPageButton has focus"); + info("Clicking the Open Login Page button"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + is( + PermissionTestUtils.testPermission(CANONICAL_URI, PERMISSION_NAME), + Services.perms.ALLOW_ACTION, + "Check permission in perm. manager if canoncial uri is set as exempt." + ); + let portalTab = await portalTabPromise; + is( + gBrowser.selectedTab, + portalTab, + "Login page should be open in a new foreground tab." + ); + is( + gBrowser.currentURI.spec, + testPath, + "Opened the right URL without upgrading it." + ); + // Close all tabs + await BrowserTestUtils.removeTab(portalTab); + let tabReloaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser); + Services.obs.notifyObservers(null, "captive-portal-login-success"); + await tabReloaded; + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js new file mode 100644 index 0000000000..9be8a3f431 --- /dev/null +++ b/browser/base/content/test/captivePortal/browser_closeCapPortalTabCanonicalURL.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +const LOGIN_LINK = `<html><body><a href="/unlock">login</a></body></html>`; +const LOGIN_URL = "http://localhost:8080/login"; +const CANONICAL_SUCCESS_URL = "http://localhost:8080/success"; +const CPS = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService +); +const META_CANONICAL_CONTENT = `<meta http-equiv=\"refresh\" content=\"0;url=http://support.mozilla.org:8080/sumo\"/>`; + +const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService +); + +let server; +let loginPageShown = false; +let showSumo = false; +const SUMO_URL = "https://support.mozilla.org:8080/sumo"; + +function redirectHandler(request, response) { + if (showSumo) { + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + META_CANONICAL_CONTENT, + META_CANONICAL_CONTENT.length + ); + return; + } + if (loginPageShown) { + return; + } + response.setStatusLine(request.httpVersion, 302, "captive"); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Location", LOGIN_URL); +} + +function sumoHandler(request, response) { + response.setHeader("Content-Type", "text/html"); + let content = ` +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + </head> + <body> + Testing + </body> +</html> +`; + response.bodyOutputStream.write(content, content.length); +} + +function loginHandler(request, response) { + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write(LOGIN_LINK, LOGIN_LINK.length); + loginPageShown = true; +} + +function unlockHandler(request, response) { + response.setStatusLine(request.httpVersion, 302, "login complete"); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Location", CANONICAL_SUCCESS_URL); +} + +add_setup(async function () { + // Set up a mock server for handling captive portal redirect. + server = new HttpServer(); + server.identity.add("http", "support.mozilla.org", 8080); + server.registerPathHandler("/success", redirectHandler); + server.registerPathHandler("/login", loginHandler); + server.registerPathHandler("/unlock", unlockHandler); + server.registerPathHandler("/sumo", sumoHandler); + server.start(8080); + registerCleanupFunction(async () => await server.stop()); + info("Mock server is now set up for captive portal redirect"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["captivedetect.canonicalURL", CANONICAL_SUCCESS_URL], + ["captivedetect.canonicalContent", META_CANONICAL_CONTENT], + ["network.dns.native-is-localhost", true], + ], + }); +}); + +// This test checks if the captive portal tab is removed after the +// sucess/abort events are fired, assuming the tab has already redirected +// to the canonical URL before they are fired. +add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_one() { + await portalDetected(); + let errorTab = await openCaptivePortalErrorTab(); + let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL); + let browser = tab.linkedBrowser; + + let redirectedToCanonicalURL = BrowserTestUtils.browserLoaded( + browser, + false, + CANONICAL_SUCCESS_URL + ); + let errorPageReloaded = BrowserTestUtils.waitForErrorPage( + errorTab.linkedBrowser + ); + + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + let loginButton = doc.querySelector("a"); + await ContentTaskUtils.waitForCondition( + () => loginButton, + "Login button on the captive portal tab is visible" + ); + info("Clicking the login button on the captive portal tab page"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + await redirectedToCanonicalURL; + info( + "Re-direct to canonical URL in the captive portal tab was succcessful after login" + ); + + let tabClosed = BrowserTestUtils.waitForTabClosing(tab); + Services.obs.notifyObservers(null, "captive-portal-login-success"); + await tabClosed; + info( + "Captive portal tab was closed on re-direct to canonical URL after login as expected" + ); + + await errorPageReloaded; + info("Captive portal error page was reloaded"); + gBrowser.removeTab(errorTab); +}); + +// This test checks if the captive portal tab is removed on location change +// i.e. when it is re-directed to the canonical URL long after success/abort +// event handlers are executed. +add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_two() { + loginPageShown = false; + await portalDetected(); + let errorTab = await openCaptivePortalErrorTab(); + let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL); + let browser = tab.linkedBrowser; + + let redirectedToCanonicalURL = BrowserTestUtils.waitForLocationChange( + gBrowser, + CANONICAL_SUCCESS_URL + ); + let errorPageReloaded = BrowserTestUtils.waitForErrorPage( + errorTab.linkedBrowser + ); + + Services.obs.notifyObservers(null, "captive-portal-login-success"); + await TestUtils.waitForCondition( + () => CPS.state == CPS.UNLOCKED_PORTAL, + "Captive portal is released" + ); + + let tabClosed = BrowserTestUtils.waitForTabClosing(tab); + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + let loginButton = doc.querySelector("a"); + await ContentTaskUtils.waitForCondition( + () => loginButton, + "Login button on the captive portal tab is visible" + ); + info("Clicking the login button on the captive portal tab page"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + await redirectedToCanonicalURL; + info( + "Re-direct to canonical URL in the captive portal tab was succcessful after login" + ); + await tabClosed; + info( + "Captive portal tab was closed on re-direct to canonical URL after login as expected" + ); + + await errorPageReloaded; + info("Captive portal error page was reloaded"); + gBrowser.removeTab(errorTab); +}); + +// Test that the captive portal page is closed after a successful login +// even if it's self-refreshed to support.mozilla.org +add_task(async function checkCaptivePortalTabCloseOnCanonicalURL_three() { + loginPageShown = false; + await portalDetected(); + let errorTab = await openCaptivePortalErrorTab(); + let tab = await openCaptivePortalLoginTab(errorTab, LOGIN_URL); + let browser = tab.linkedBrowser; + + let errorPageReloaded = BrowserTestUtils.waitForErrorPage( + errorTab.linkedBrowser + ); + + let tabClosed = BrowserTestUtils.waitForTabClosing(tab); + showSumo = true; + await SpecialPowers.spawn(browser, [], async () => { + let doc = content.document; + let loginButton = doc.querySelector("a"); + await ContentTaskUtils.waitForCondition( + () => loginButton, + "Login button on the captive portal tab is visible" + ); + info("Clicking the login button on the captive portal tab page"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + Services.obs.notifyObservers(null, "captive-portal-login-success"); + + await TestUtils.waitForCondition( + () => CPS.state == CPS.UNLOCKED_PORTAL, + "Captive portal is released" + ); + + await tabClosed; + info( + "Captive portal tab was closed on re-direct to canonical URL after login as expected" + ); + + await errorPageReloaded; + info("Captive portal error page was reloaded"); + showSumo = false; + gBrowser.removeTab(errorTab); +}); diff --git a/browser/base/content/test/captivePortal/head.js b/browser/base/content/test/captivePortal/head.js new file mode 100644 index 0000000000..77105456a2 --- /dev/null +++ b/browser/base/content/test/captivePortal/head.js @@ -0,0 +1,270 @@ +XPCOMUtils.defineLazyServiceGetter( + this, + "cps", + "@mozilla.org/network/captive-portal-service;1", + "nsICaptivePortalService" +); + +const CANONICAL_CONTENT = "success"; +const CANONICAL_URL = "data:text/plain;charset=utf-8," + CANONICAL_CONTENT; +const CANONICAL_URL_REDIRECTED = "data:text/plain;charset=utf-8,redirected"; +const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected"; +const BAD_CERT_PAGE = "https://expired.example.com/"; + +async function setupPrefsAndRecentWindowBehavior() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["captivedetect.canonicalURL", CANONICAL_URL], + ["captivedetect.canonicalContent", CANONICAL_CONTENT], + ], + }); + // We need to test behavior when a portal is detected when there is no browser + // window, but we can't close the default window opened by the test harness. + // Instead, we deactivate CaptivePortalWatcher in the default window and + // exclude it using an attribute to mask its presence. + window.CaptivePortalWatcher.uninit(); + window.document.documentElement.setAttribute("ignorecaptiveportal", "true"); + + registerCleanupFunction(function cleanUp() { + window.CaptivePortalWatcher.init(); + window.document.documentElement.removeAttribute("ignorecaptiveportal"); + }); +} + +async function portalDetected() { + Services.obs.notifyObservers(null, "captive-portal-login"); + await TestUtils.waitForCondition(() => { + return cps.state == cps.LOCKED_PORTAL; + }, "Waiting for Captive Portal Service to update state after portal detected."); +} + +async function freePortal(aSuccess) { + Services.obs.notifyObservers( + null, + "captive-portal-login-" + (aSuccess ? "success" : "abort") + ); + await TestUtils.waitForCondition(() => { + return cps.state != cps.LOCKED_PORTAL; + }, "Waiting for Captive Portal Service to update state after portal freed."); +} + +// If a window is provided, it will be focused. Otherwise, a new window +// will be opened and focused. +async function focusWindowAndWaitForPortalUI(aLongRecheck, win) { + // CaptivePortalWatcher triggers a recheck when a window gains focus. If + // the time taken for the check to complete is under PORTAL_RECHECK_DELAY_MS, + // a tab with the login page is opened and selected. If it took longer, + // no tab is opened. It's not reliable to time things in an async test, + // so use a delay threshold of -1 to simulate a long recheck (so that any + // amount of time is considered excessive), and a very large threshold to + // simulate a short recheck. + Services.prefs.setIntPref( + "captivedetect.portalRecheckDelayMS", + aLongRecheck ? -1 : 1000000 + ); + + if (!win) { + win = await BrowserTestUtils.openNewBrowserWindow(); + } + let windowActivePromise = waitForBrowserWindowActive(win); + win.focus(); + await windowActivePromise; + + // After a new window is opened, CaptivePortalWatcher asks for a recheck, and + // waits for it to complete. We need to manually tell it a recheck completed. + await TestUtils.waitForCondition(() => { + return win.CaptivePortalWatcher._waitingForRecheck; + }, "Waiting for CaptivePortalWatcher to trigger a recheck."); + Services.obs.notifyObservers(null, "captive-portal-check-complete"); + + let notification = await ensurePortalNotification(win); + + if (aLongRecheck) { + ensureNoPortalTab(win); + await testShowLoginPageButtonVisibility(notification, "visible"); + return win; + } + + let tab = win.gBrowser.tabs[1]; + if (tab.linkedBrowser.currentURI.spec != CANONICAL_URL) { + // The tab should load the canonical URL, wait for it. + await BrowserTestUtils.waitForLocationChange(win.gBrowser, CANONICAL_URL); + } + is( + win.gBrowser.selectedTab, + tab, + "The captive portal tab should be open and selected in the new window." + ); + await testShowLoginPageButtonVisibility(notification, "hidden"); + return win; +} + +function ensurePortalTab(win) { + // For the tests that call this function, it's enough to ensure there + // are two tabs in the window - the default tab and the portal tab. + is( + win.gBrowser.tabs.length, + 2, + "There should be a captive portal tab in the window." + ); +} + +async function ensurePortalNotification(win) { + await BrowserTestUtils.waitForMutationCondition( + win.gNavToolbox, + { childList: true }, + () => + win.gNavToolbox + .querySelector("notification-message") + ?.getAttribute("value") == PORTAL_NOTIFICATION_VALUE + ); + + let notification = win.gNotificationBox.getNotificationWithValue( + PORTAL_NOTIFICATION_VALUE + ); + isnot( + notification, + null, + "There should be a captive portal notification in the window." + ); + return notification; +} + +// Helper to test whether the "Show Login Page" is visible in the captive portal +// notification (it should be hidden when the portal tab is selected). +async function testShowLoginPageButtonVisibility(notification, visibility) { + await notification.updateComplete; + let showLoginPageButton = notification.buttonContainer.querySelector( + "button.notification-button" + ); + // If the visibility property was never changed from default, it will be + // an empty string, so we pretend it's "visible" (effectively the same). + is( + showLoginPageButton.style.visibility || "visible", + visibility, + 'The "Show Login Page" button should be ' + visibility + "." + ); +} + +function ensureNoPortalTab(win) { + is( + win.gBrowser.tabs.length, + 1, + "There should be no captive portal tab in the window." + ); +} + +function ensureNoPortalNotification(win) { + is( + win.gNotificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE), + null, + "There should be no captive portal notification in the window." + ); +} + +/** + * Some tests open a new window and close it later. When the window is closed, + * the original window opened by mochitest gains focus, generating an + * activate event. If the next test also opens a new window + * before this event has a chance to fire, CaptivePortalWatcher picks + * up the first one instead of the one from the new window. To avoid this + * unfortunate intermittent timing issue, we wait for the event from + * the original window every time we close a window that we opened. + */ +function waitForBrowserWindowActive(win) { + return new Promise(resolve => { + if (Services.focus.activeWindow == win) { + resolve(); + } else { + win.addEventListener( + "activate", + () => { + resolve(); + }, + { once: true } + ); + } + }); +} + +async function closeWindowAndWaitForWindowActivate(win) { + let activationPromises = []; + for (let w of BrowserWindowTracker.orderedWindows) { + if ( + w != win && + !win.document.documentElement.getAttribute("ignorecaptiveportal") + ) { + activationPromises.push(waitForBrowserWindowActive(win)); + } + } + await BrowserTestUtils.closeWindow(win); + await Promise.race(activationPromises); +} + +/** + * BrowserTestUtils.openNewBrowserWindow() does not guarantee the newly + * opened window has received focus when the promise resolves, so we + * have to manually wait every time. + */ +async function openWindowAndWaitForFocus() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await waitForBrowserWindowActive(win); + return win; +} + +async function openCaptivePortalErrorTab() { + // Open a page with a cert error. + let browser; + let certErrorLoaded; + let errorTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + () => { + let tab = BrowserTestUtils.addTab(gBrowser, BAD_CERT_PAGE); + gBrowser.selectedTab = tab; + browser = gBrowser.selectedBrowser; + certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser); + return tab; + }, + false + ); + await certErrorLoaded; + info("A cert error page was opened"); + await SpecialPowers.spawn(errorTab.linkedBrowser, [], async () => { + let doc = content.document; + let loginButton = doc.getElementById("openPortalLoginPageButton"); + await ContentTaskUtils.waitForCondition( + () => loginButton && doc.body.className == "captiveportal", + "Captive portal error page UI is visible" + ); + }); + info("Captive portal error page UI is visible"); + + return errorTab; +} + +async function openCaptivePortalLoginTab( + errorTab, + LOGIN_PAGE_URL = CANONICAL_URL +) { + let portalTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + LOGIN_PAGE_URL, + true + ); + + await SpecialPowers.spawn(errorTab.linkedBrowser, [], async () => { + let doc = content.document; + let loginButton = doc.getElementById("openPortalLoginPageButton"); + info("Click on the login button on the captive portal error page"); + await EventUtils.synthesizeMouseAtCenter(loginButton, {}, content); + }); + + let portalTab = await portalTabPromise; + is( + gBrowser.selectedTab, + portalTab, + "Captive Portal login page is now open in a new foreground tab." + ); + + return portalTab; +} |