diff options
Diffstat (limited to '')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt | 3126 |
1 files changed, 3126 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt new file mode 100644 index 0000000000..f688b498f5 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt @@ -0,0 +1,3126 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test + +import android.graphics.Bitmap +import android.os.Looper +import android.os.SystemClock +import android.util.Base64 +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports +import org.json.JSONObject +import org.junit.Assume.assumeThat +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.geckoview.* // ktlint-disable no-wildcard-imports +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.ContentDelegate +import org.mozilla.geckoview.GeckoSession.HistoryDelegate +import org.mozilla.geckoview.GeckoSession.Loader +import org.mozilla.geckoview.GeckoSession.NavigationDelegate +import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest +import org.mozilla.geckoview.GeckoSession.PermissionDelegate +import org.mozilla.geckoview.GeckoSession.ProgressDelegate +import org.mozilla.geckoview.GeckoSession.TextInputDelegate +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.* // ktlint-disable no-wildcard-imports +import org.mozilla.geckoview.test.util.UiThreadUtils +import java.io.ByteArrayOutputStream +import java.util.concurrent.ThreadLocalRandom +import kotlin.concurrent.thread + +@RunWith(AndroidJUnit4::class) +@MediumTest +class NavigationDelegateTest : BaseSessionTest() { + + // Provides getters for Loader + class TestLoader : Loader() { + var mUri: String? = null + override fun uri(uri: String): TestLoader { + mUri = uri + super.uri(uri) + return this + } + fun getUri(): String? { + return mUri + } + override fun flags(f: Int): TestLoader { + super.flags(f) + return this + } + } + + fun testLoadErrorWithErrorPage( + testLoader: TestLoader, + expectedCategory: Int, + expectedError: Int, + errorPageUrl: String?, + ) { + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "URI should be " + testLoader.getUri(), + request.uri, + equalTo(testLoader.getUri()), + ) + assertThat( + "App requested this load", + request.isDirectNavigation, + equalTo(true), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat( + "URI should be " + testLoader.getUri(), + url, + equalTo(testLoader.getUri()), + ) + } + + @AssertCalled(count = 1, order = [3]) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + assertThat( + "Error category should match", + error.category, + equalTo(expectedCategory), + ) + assertThat( + "Error code should match", + error.code, + equalTo(expectedError), + ) + return GeckoResult.fromValue(errorPageUrl) + } + + @AssertCalled(count = 1, order = [4]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should fail", success, equalTo(false)) + } + }, + ) + + mainSession.load(testLoader) + sessionRule.waitForPageStop() + + if (errorPageUrl != null) { + sessionRule.waitUntilCalled(object : ContentDelegate, NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, equalTo(testLoader.getUri())) + } + + @AssertCalled(count = 1, order = [2]) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should not be empty", title, not(isEmptyOrNullString())) + } + }) + } + } + + fun testLoadExpectError( + testUri: String, + expectedCategory: Int, + expectedError: Int, + ) { + testLoadExpectError(TestLoader().uri(testUri), expectedCategory, expectedError) + } + + fun testLoadExpectError( + testLoader: TestLoader, + expectedCategory: Int, + expectedError: Int, + ) { + testLoadErrorWithErrorPage( + testLoader, + expectedCategory, + expectedError, + createTestUrl(HELLO_HTML_PATH), + ) + testLoadErrorWithErrorPage( + testLoader, + expectedCategory, + expectedError, + null, + ) + } + + fun testLoadEarlyErrorWithErrorPage( + testUri: String, + expectedCategory: Int, + expectedError: Int, + errorPageUrl: String?, + ) { + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + + @AssertCalled(false) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat("URI should be " + testUri, url, equalTo(testUri)) + } + + @AssertCalled(count = 1, order = [1]) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + assertThat( + "Error category should match", + error.category, + equalTo(expectedCategory), + ) + assertThat( + "Error code should match", + error.code, + equalTo(expectedError), + ) + return GeckoResult.fromValue(errorPageUrl) + } + + @AssertCalled(false) + override fun onPageStop(session: GeckoSession, success: Boolean) { + } + }, + ) + + mainSession.loadUri(testUri) + sessionRule.waitUntilCalled(NavigationDelegate::class, "onLoadError") + + if (errorPageUrl != null) { + sessionRule.waitUntilCalled(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should not be empty", title, not(isEmptyOrNullString())) + } + }) + } + } + + fun testLoadEarlyError( + testUri: String, + expectedCategory: Int, + expectedError: Int, + ) { + testLoadEarlyErrorWithErrorPage(testUri, expectedCategory, expectedError, createTestUrl(HELLO_HTML_PATH)) + testLoadEarlyErrorWithErrorPage(testUri, expectedCategory, expectedError, null) + } + + @Test fun loadFileNotFound() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + testLoadExpectError( + "file:///test.mozilla", + WebRequestError.ERROR_CATEGORY_URI, + WebRequestError.ERROR_FILE_NOT_FOUND, + ) + + val promise = mainSession.evaluatePromiseJS("document.addCertException(false)") + var exceptionCaught = false + try { + val result = promise.value as Boolean + assertThat("Promise should not resolve", result, equalTo(false)) + } catch (e: GeckoSessionTestRule.RejectedPromiseException) { + exceptionCaught = true + } + assertThat("document.addCertException failed with exception", exceptionCaught, equalTo(true)) + } + + @Test fun loadUnknownHost() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + testLoadExpectError( + UNKNOWN_HOST_URI, + WebRequestError.ERROR_CATEGORY_URI, + WebRequestError.ERROR_UNKNOWN_HOST, + ) + } + + // External loads should not have access to privileged protocols + @Test fun loadExternalDenied() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + testLoadExpectError( + TestLoader() + .uri("file:///") + .flags(GeckoSession.LOAD_FLAGS_EXTERNAL), + WebRequestError.ERROR_CATEGORY_UNKNOWN, + WebRequestError.ERROR_UNKNOWN, + ) + testLoadExpectError( + TestLoader() + .uri("resource://gre/") + .flags(GeckoSession.LOAD_FLAGS_EXTERNAL), + WebRequestError.ERROR_CATEGORY_UNKNOWN, + WebRequestError.ERROR_UNKNOWN, + ) + testLoadExpectError( + TestLoader() + .uri("about:about") + .flags(GeckoSession.LOAD_FLAGS_EXTERNAL), + WebRequestError.ERROR_CATEGORY_UNKNOWN, + WebRequestError.ERROR_UNKNOWN, + ) + testLoadExpectError( + TestLoader() + .uri("resource://android/assets/web_extensions/") + .flags(GeckoSession.LOAD_FLAGS_EXTERNAL), + WebRequestError.ERROR_CATEGORY_UNKNOWN, + WebRequestError.ERROR_UNKNOWN, + ) + } + + @Test fun loadInvalidUri() { + testLoadEarlyError( + INVALID_URI, + WebRequestError.ERROR_CATEGORY_URI, + WebRequestError.ERROR_MALFORMED_URI, + ) + } + + @Test fun loadBadPort() { + testLoadEarlyError( + "http://localhost:1/", + WebRequestError.ERROR_CATEGORY_NETWORK, + WebRequestError.ERROR_PORT_BLOCKED, + ) + } + + @Test fun loadUntrusted() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + val host = if (sessionRule.env.isAutomation) { + "expired.example.com" + } else { + "expired.badssl.com" + } + val uri = "https://$host/" + testLoadExpectError( + uri, + WebRequestError.ERROR_CATEGORY_SECURITY, + WebRequestError.ERROR_SECURITY_BAD_CERT, + ) + + mainSession.waitForJS("document.addCertException(false)") + mainSession.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat("URI should be " + uri, url, equalTo(uri)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onSecurityChange( + session: GeckoSession, + securityInfo: ProgressDelegate.SecurityInformation, + ) { + assertThat("Should be exception", securityInfo.isException, equalTo(true)) + assertThat("Should not be secure", securityInfo.isSecure, equalTo(false)) + } + + @AssertCalled(count = 1, order = [3]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should succeed", success, equalTo(true)) + sessionRule.removeAllCertOverrides() + } + }, + ) + mainSession.evaluateJS("location.reload()") + mainSession.waitForPageStop() + } + + @Test fun loadWithHTTPSOnlyMode() { + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.HTTPS_ONLY) + + val httpsFirstPref = "dom.security.https_first" + val httpsFirstPrefValue = (sessionRule.getPrefs(httpsFirstPref)[0] as Boolean) + + val httpsFirstPBMPref = "dom.security.https_first_pbm" + val httpsFirstPBMPrefValue = (sessionRule.getPrefs(httpsFirstPBMPref)[0] as Boolean) + + val insecureUri = if (sessionRule.env.isAutomation) { + "http://nocert.example.com/" + } else { + "http://neverssl.com" + } + + val secureUri = if (sessionRule.env.isAutomation) { + "http://example.com/" + } else { + "http://neverssl.com" + } + + mainSession.loadUri(insecureUri) + mainSession.waitForPageStop() + + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + assertThat("categories should match", error.category, equalTo(WebRequestError.ERROR_CATEGORY_NETWORK)) + assertThat("codes should match", error.code, equalTo(WebRequestError.ERROR_HTTPS_ONLY)) + return null + } + }) + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL) + + mainSession.loadUri(secureUri) + mainSession.waitForPageStop() + + var onLoadCalledCounter = 0 + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 0) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + return null + } + + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + onLoadCalledCounter++ + return null + } + }) + + if (httpsFirstPrefValue) { + // if https-first is enabled we get two calls to onLoadRequest + // (1) http://example.com/ and (2) https://example.com/ + assertThat("Assert count mainSession.onLoadRequest", onLoadCalledCounter, equalTo(2)) + } else { + assertThat("Assert count mainSession.onLoadRequest", onLoadCalledCounter, equalTo(1)) + } + + val privateSession = sessionRule.createOpenSession( + GeckoSessionSettings.Builder(mainSession.settings) + .usePrivateMode(true) + .build(), + ) + + privateSession.loadUri(secureUri) + privateSession.waitForPageStop() + + onLoadCalledCounter = 0 + privateSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 0) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + return null + } + + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + onLoadCalledCounter++ + return null + } + }) + + if (httpsFirstPBMPrefValue) { + // if https-first is enabled we get two calls to onLoadRequest + // (1) http://example.com/ and (2) https://example.com/ + assertThat("Assert count privateSession.onLoadRequest", onLoadCalledCounter, equalTo(2)) + } else { + assertThat("Assert count privateSession.onLoadRequest", onLoadCalledCounter, equalTo(1)) + } + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.HTTPS_ONLY_PRIVATE) + + privateSession.loadUri(insecureUri) + privateSession.waitForPageStop() + + privateSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + assertThat("categories should match", error.category, equalTo(WebRequestError.ERROR_CATEGORY_NETWORK)) + assertThat("codes should match", error.code, equalTo(WebRequestError.ERROR_HTTPS_ONLY)) + return null + } + }) + + mainSession.loadUri(secureUri) + mainSession.waitForPageStop() + + onLoadCalledCounter = 0 + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 0) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + return null + } + + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + onLoadCalledCounter++ + return null + } + }) + + if (httpsFirstPrefValue) { + // if https-first is enabled we get two calls to onLoadRequest + // (1) http://example.com/ and (2) https://example.com/ + assertThat("Assert count mainSession.onLoadRequest", onLoadCalledCounter, equalTo(2)) + } else { + assertThat("Assert count mainSession.onLoadRequest", onLoadCalledCounter, equalTo(1)) + } + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL) + } + + // Due to Bug 1692578 we currently cannot test bypassing of the error + // the URI loading process takes the desktop path for iframes + @Test fun loadHTTPSOnlyInSubframe() { + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.HTTPS_ONLY) + + val uri = "http://example.org/tests/junit/iframe_http_only.html" + val httpsUri = "https://example.org/tests/junit/iframe_http_only.html" + val iFrameUri = "http://expired.example.com/" + val iFrameHttpsUri = "https://expired.example.com/" + + val testLoader = TestLoader().uri(uri) + + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("The URLs must match", request.uri, equalTo(forEachCall(uri, httpsUri))) + return null + } + + @AssertCalled(count = 1) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat( + "URI should be " + uri, + url, + equalTo(uri), + ) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should fail", success, equalTo(true)) + } + + @AssertCalled(count = 2) + override fun onSubframeLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat("URI should match", request.uri, equalTo(forEachCall(iFrameUri, iFrameHttpsUri))) + return GeckoResult.allow() + } + }, + ) + + mainSession.load(testLoader) + sessionRule.waitForPageStop() + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL) + } + + @Test fun bypassHTTPSOnlyError() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.HTTPS_ONLY) + + val host = if (sessionRule.env.isAutomation) { + "expired.example.com" + } else { + "expired.badssl.com" + } + + val uri = "http://$host/" + val httpsUri = "https://$host/" + + val testLoader = TestLoader().uri(uri) + + // The two loads below follow testLoadExpectError(TestLoader, Int, Int) flow + + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("The URLs must match", request.uri, equalTo(forEachCall(uri, httpsUri))) + return null + } + + @AssertCalled(count = 1) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat( + "URI should be " + uri, + url, + equalTo(uri), + ) + } + + @AssertCalled(count = 1) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + assertThat( + "Error code should match", + error.code, + equalTo(WebRequestError.ERROR_HTTPS_ONLY), + ) + return GeckoResult.fromValue(createTestUrl(HELLO_HTML_PATH)) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should fail", success, equalTo(false)) + } + }, + ) + + mainSession.load(testLoader) + sessionRule.waitForPageStop() + + sessionRule.waitUntilCalled(object : ContentDelegate, NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, equalTo(httpsUri)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should not be empty", title, not(isEmptyOrNullString())) + } + }) + + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 2, order = [1, 3]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("The URLs must match", request.uri, equalTo(forEachCall(uri, httpsUri))) + return null + } + + @AssertCalled(count = 1, order = [4]) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + assertThat( + "Error code should match", + error.code, + equalTo(WebRequestError.ERROR_HTTPS_ONLY), + ) + return GeckoResult.fromValue(null) + } + + @AssertCalled(count = 1, order = [5]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should fail", success, equalTo(false)) + } + }, + ) + + mainSession.load(testLoader) + sessionRule.waitForPageStop() + + sessionRule.delegateDuringNextWait( + object : ProgressDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 1) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + // We set http scheme only in case it's not iFrame + assertThat("The URLs must match", request.uri, equalTo(uri)) + return null + } + + @AssertCalled(count = 0) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + + mainSession.waitForJS("document.reloadWithHttpsOnlyException()") + mainSession.waitForPageStop() + + sessionRule.runtime.settings.setAllowInsecureConnections(GeckoRuntimeSettings.ALLOW_ALL) + } + + @Test fun loadHSTSBadCert() { + val httpsFirstPref = "dom.security.https_first" + assertThat("https pref should be false", sessionRule.getPrefs(httpsFirstPref)[0] as Boolean, equalTo(false)) + + // load secure url with hsts header + val uri = "https://example.com/tests/junit/hsts_header.sjs" + mainSession.loadUri(uri) + mainSession.waitForPageStop() + + // load insecure subdomain url to see if it gets upgraded to https + val http_uri = "http://test1.example.com/" + val https_uri = "https://test1.example.com/" + + mainSession.loadUri(http_uri) + mainSession.waitForPageStop() + + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "URI should be HTTP then redirected to HTTPS", + request.uri, + equalTo(forEachCall(http_uri, https_uri)), + ) + return null + } + }) + + // load subdomain that will trigger the cert error + val no_cert_uri = "https://nocert.example.com/" + mainSession.loadUri(no_cert_uri) + mainSession.waitForPageStop() + + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + assertThat("categories should match", error.category, equalTo(WebRequestError.ERROR_CATEGORY_NETWORK)) + assertThat("codes should match", error.code, equalTo(WebRequestError.ERROR_BAD_HSTS_CERT)) + return null + } + }) + sessionRule.clearHSTSState() + } + + @Ignore // Disabled for bug 1619344. + @Test + fun loadUnknownProtocol() { + testLoadEarlyError( + UNKNOWN_PROTOCOL_URI, + WebRequestError.ERROR_CATEGORY_URI, + WebRequestError.ERROR_UNKNOWN_PROTOCOL, + ) + } + + // Due to Bug 1692578 we currently cannot test displaying the error + // the URI loading process takes the desktop path for iframes + @Test fun loadUnknownProtocolIframe() { + // Should match iframe URI from IFRAME_UNKNOWN_PROTOCOL + val iframeUri = "foo://bar" + mainSession.loadTestPath(IFRAME_UNKNOWN_PROTOCOL) + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat("URI should match", request.uri, endsWith(IFRAME_UNKNOWN_PROTOCOL)) + return null + } + + @AssertCalled(count = 1) + override fun onSubframeLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat("URI should match", request.uri, endsWith(iframeUri)) + return null + } + }) + } + + @Setting(key = Setting.Key.USE_TRACKING_PROTECTION, value = "true") + @Ignore + // TODO: Bug 1564373 + @Test + fun trackingProtection() { + val category = ContentBlocking.AntiTracking.TEST + sessionRule.runtime.settings.contentBlocking.setAntiTracking(category) + mainSession.loadTestPath(TRACKERS_PATH) + + sessionRule.waitUntilCalled( + object : ContentBlocking.Delegate { + @AssertCalled(count = 3) + override fun onContentBlocked( + session: GeckoSession, + event: ContentBlocking.BlockEvent, + ) { + assertThat( + "Category should be set", + event.antiTrackingCategory, + equalTo(category), + ) + assertThat("URI should not be null", event.uri, notNullValue()) + assertThat("URI should match", event.uri, endsWith("tracker.js")) + } + + @AssertCalled(false) + override fun onContentLoaded(session: GeckoSession, event: ContentBlocking.BlockEvent) { + } + }, + ) + + mainSession.settings.useTrackingProtection = false + + mainSession.reload() + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : ContentBlocking.Delegate { + @AssertCalled(false) + override fun onContentBlocked( + session: GeckoSession, + event: ContentBlocking.BlockEvent, + ) { + } + + @AssertCalled(count = 3) + override fun onContentLoaded(session: GeckoSession, event: ContentBlocking.BlockEvent) { + assertThat( + "Category should be set", + event.antiTrackingCategory, + equalTo(category), + ) + assertThat("URI should not be null", event.uri, notNullValue()) + assertThat("URI should match", event.uri, endsWith("tracker.js")) + } + }, + ) + } + + @Test fun redirectLoad() { + val redirectUri = if (sessionRule.env.isAutomation) { + "https://example.org/tests/junit/hello.html" + } else { + "https://jigsaw.w3.org/HTTP/300/Overview.html" + } + val uri = if (sessionRule.env.isAutomation) { + "https://example.org/tests/junit/simple_redirect.sjs?$redirectUri" + } else { + "https://jigsaw.w3.org/HTTP/300/301.html" + } + + mainSession.loadUri(uri) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 2, order = [1, 2]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("Session should not be null", session, notNullValue()) + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat( + "URL should match", + request.uri, + equalTo(forEachCall(request.uri, redirectUri)), + ) + assertThat( + "Trigger URL should be null", + request.triggerUri, + nullValue(), + ) + assertThat( + "From app should be correct", + request.isDirectNavigation, + equalTo(forEachCall(true, false)), + ) + assertThat("Target should not be null", request.target, notNullValue()) + assertThat( + "Target should match", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_CURRENT), + ) + assertThat( + "Redirect flag is set", + request.isRedirect, + equalTo(forEachCall(false, true)), + ) + return null + } + }) + } + + @Test fun redirectLoadIframe() { + val path = if (sessionRule.env.isAutomation) { + IFRAME_REDIRECT_AUTOMATION + } else { + IFRAME_REDIRECT_LOCAL + } + + mainSession.loadTestPath(path) + sessionRule.waitForPageStop() + + // We shouldn't be firing onLoadRequest for iframes, including redirects. + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("Session should not be null", session, notNullValue()) + assertThat("App requested this load", request.isDirectNavigation, equalTo(true)) + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat("URI should match", request.uri, endsWith(path)) + assertThat("isRedirect should match", request.isRedirect, equalTo(false)) + return null + } + + @AssertCalled(count = 2) + override fun onSubframeLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("Session should not be null", session, notNullValue()) + assertThat("App did not request this load", request.isDirectNavigation, equalTo(false)) + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat( + "isRedirect should match", + request.isRedirect, + equalTo(forEachCall(false, true)), + ) + return null + } + }) + } + + @Test fun redirectDenyLoad() { + val redirectUri = if (sessionRule.env.isAutomation) { + "https://example.org/tests/junit/hello.html" + } else { + "https://jigsaw.w3.org/HTTP/300/Overview.html" + } + val uri = if (sessionRule.env.isAutomation) { + "https://example.org/tests/junit/simple_redirect.sjs?$redirectUri" + } else { + "https://jigsaw.w3.org/HTTP/300/301.html" + } + + sessionRule.delegateDuringNextWait( + object : NavigationDelegate { + @AssertCalled(count = 2, order = [1, 2]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("Session should not be null", session, notNullValue()) + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat( + "URL should match", + request.uri, + equalTo(forEachCall(request.uri, redirectUri)), + ) + assertThat( + "Trigger URL should be null", + request.triggerUri, + nullValue(), + ) + assertThat( + "From app should be correct", + request.isDirectNavigation, + equalTo(forEachCall(true, false)), + ) + assertThat("Target should not be null", request.target, notNullValue()) + assertThat( + "Target should match", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_CURRENT), + ) + assertThat( + "Redirect flag is set", + request.isRedirect, + equalTo(forEachCall(false, true)), + ) + + return forEachCall(GeckoResult.allow(), GeckoResult.deny()) + } + }, + ) + + mainSession.loadUri(uri) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : ProgressDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat("URL should match", url, equalTo(uri)) + } + }, + ) + } + + @Test fun redirectIntentLoad() { + assumeThat(sessionRule.env.isAutomation, equalTo(true)) + + val redirectUri = "intent://test" + val uri = "https://example.org/tests/junit/simple_redirect.sjs?$redirectUri" + + mainSession.loadUri(uri) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 2, order = [1, 2]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("URL should match", request.uri, equalTo(forEachCall(uri, redirectUri))) + assertThat( + "From app should be correct", + request.isDirectNavigation, + equalTo(forEachCall(true, false)), + ) + return null + } + }) + } + + @Test fun bypassClassifier() { + val phishingUri = "https://www.itisatrap.org/firefox/its-a-trap.html" + val category = ContentBlocking.SafeBrowsing.PHISHING + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(category) + + mainSession.load( + Loader() + .uri(phishingUri + "?bypass=true") + .flags(GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER), + ) + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + } + + @Test fun safebrowsingPhishing() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + val phishingUri = "https://www.itisatrap.org/firefox/its-a-trap.html" + val category = ContentBlocking.SafeBrowsing.PHISHING + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(category) + + // Add query string to avoid bypassing classifier check because of cache. + testLoadExpectError( + phishingUri + "?block=true", + WebRequestError.ERROR_CATEGORY_SAFEBROWSING, + WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI, + ) + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(ContentBlocking.SafeBrowsing.NONE) + + mainSession.loadUri(phishingUri + "?block=false") + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + } + + @Test fun safebrowsingMalware() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + val malwareUri = "https://www.itisatrap.org/firefox/its-an-attack.html" + val category = ContentBlocking.SafeBrowsing.MALWARE + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(category) + + testLoadExpectError( + malwareUri + "?block=true", + WebRequestError.ERROR_CATEGORY_SAFEBROWSING, + WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI, + ) + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(ContentBlocking.SafeBrowsing.NONE) + + mainSession.loadUri(malwareUri + "?block=false") + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + } + + @Test fun safebrowsingUnwanted() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + val unwantedUri = "https://www.itisatrap.org/firefox/unwanted.html" + val category = ContentBlocking.SafeBrowsing.UNWANTED + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(category) + + testLoadExpectError( + unwantedUri + "?block=true", + WebRequestError.ERROR_CATEGORY_SAFEBROWSING, + WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI, + ) + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(ContentBlocking.SafeBrowsing.NONE) + + mainSession.loadUri(unwantedUri + "?block=false") + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + } + + @Test fun safebrowsingHarmful() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + val harmfulUri = "https://www.itisatrap.org/firefox/harmful.html" + val category = ContentBlocking.SafeBrowsing.HARMFUL + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(category) + + testLoadExpectError( + harmfulUri + "?block=true", + WebRequestError.ERROR_CATEGORY_SAFEBROWSING, + WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI, + ) + + sessionRule.runtime.settings.contentBlocking.setSafeBrowsing(ContentBlocking.SafeBrowsing.NONE) + + mainSession.loadUri(harmfulUri + "?block=false") + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }, + ) + } + + // Checks that the User Agent matches the user agent built in + // nsHttpHandler::BuildUserAgent + @Test fun defaultUserAgentMatchesActualUserAgent() { + var userAgent = sessionRule.waitForResult(mainSession.userAgent) + assertThat( + "Mobile user agent should match the default user agent", + userAgent, + equalTo(GeckoSession.getDefaultUserAgent()), + ) + } + + @Test fun desktopMode() { + mainSession.loadUri("https://example.com") + sessionRule.waitForPageStop() + + val mobileSubStr = "Mobile" + val desktopSubStr = "X11" + + assertThat( + "User agent should be set to mobile", + getUserAgent(), + containsString(mobileSubStr), + ) + + var userAgent = sessionRule.waitForResult(mainSession.userAgent) + assertThat( + "User agent should be reported as mobile", + userAgent, + containsString(mobileSubStr), + ) + + mainSession.settings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_DESKTOP + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should be set to desktop", + getUserAgent(), + containsString(desktopSubStr), + ) + + userAgent = sessionRule.waitForResult(mainSession.userAgent) + assertThat( + "User agent should be reported as desktop", + userAgent, + containsString(desktopSubStr), + ) + + mainSession.settings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should be set to mobile", + getUserAgent(), + containsString(mobileSubStr), + ) + + userAgent = sessionRule.waitForResult(mainSession.userAgent) + assertThat( + "User agent should be reported as mobile", + userAgent, + containsString(mobileSubStr), + ) + + val vrSubStr = "Mobile VR" + mainSession.settings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_VR + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should be set to VR", + getUserAgent(), + containsString(vrSubStr), + ) + + userAgent = sessionRule.waitForResult(mainSession.userAgent) + assertThat( + "User agent should be reported as VR", + userAgent, + containsString(vrSubStr), + ) + } + + private fun getUserAgent(session: GeckoSession = mainSession): String { + return session.evaluateJS("window.navigator.userAgent") as String + } + + @Test fun uaOverrideNewSession() { + val newSession = sessionRule.createClosedSession() + newSession.settings.userAgentOverride = "Test user agent override" + + newSession.open() + newSession.loadUri("https://example.com") + newSession.waitForPageStop() + + assertThat( + "User agent should match override", + getUserAgent(newSession), + equalTo("Test user agent override"), + ) + } + + @Test fun uaOverride() { + mainSession.loadUri("https://example.com") + sessionRule.waitForPageStop() + + val mobileSubStr = "Mobile" + val vrSubStr = "Mobile VR" + val overrideUserAgent = "This is the override user agent" + + assertThat( + "User agent should be reported as mobile", + getUserAgent(), + containsString(mobileSubStr), + ) + + mainSession.settings.userAgentOverride = overrideUserAgent + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should be reported as override", + getUserAgent(), + equalTo(overrideUserAgent), + ) + + mainSession.settings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_VR + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should still be reported as override even when USER_AGENT_MODE is set", + getUserAgent(), + equalTo(overrideUserAgent), + ) + + mainSession.settings.userAgentOverride = null + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should now be reported as VR", + getUserAgent(), + containsString(vrSubStr), + ) + + sessionRule.delegateDuringNextWait(object : NavigationDelegate { + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + mainSession.settings.userAgentOverride = overrideUserAgent + return null + } + }) + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should be reported as override after being set in onLoadRequest", + getUserAgent(), + equalTo(overrideUserAgent), + ) + + sessionRule.delegateDuringNextWait(object : NavigationDelegate { + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + mainSession.settings.userAgentOverride = null + return null + } + }) + + mainSession.reload() + mainSession.waitForPageStop() + + assertThat( + "User agent should again be reported as VR after disabling override in onLoadRequest", + getUserAgent(), + containsString(vrSubStr), + ) + } + + @WithDisplay(width = 600, height = 200) + @Test + fun viewportMode() { + mainSession.loadTestPath(VIEWPORT_PATH) + sessionRule.waitForPageStop() + + val desktopInnerWidth = 980.0 + val physicalWidth = 600.0 + val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double + val mobileInnerWidth = physicalWidth / pixelRatio + val innerWidthJs = "window.innerWidth" + + var innerWidth = mainSession.evaluateJS(innerWidthJs) as Double + assertThat( + "innerWidth should be equal to $mobileInnerWidth", + innerWidth, + closeTo(mobileInnerWidth, 0.1), + ) + + mainSession.settings.viewportMode = GeckoSessionSettings.VIEWPORT_MODE_DESKTOP + + mainSession.reload() + mainSession.waitForPageStop() + + innerWidth = mainSession.evaluateJS(innerWidthJs) as Double + assertThat( + "innerWidth should be equal to $desktopInnerWidth", + innerWidth, + closeTo(desktopInnerWidth, 0.1), + ) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + innerWidth = mainSession.evaluateJS(innerWidthJs) as Double + assertThat( + "after navigation innerWidth should be equal to $desktopInnerWidth", + innerWidth, + closeTo(desktopInnerWidth, 0.1), + ) + + mainSession.loadTestPath(VIEWPORT_PATH) + sessionRule.waitForPageStop() + + innerWidth = mainSession.evaluateJS(innerWidthJs) as Double + assertThat( + "after navigting back innerWidth should be equal to $desktopInnerWidth", + innerWidth, + closeTo(desktopInnerWidth, 0.1), + ) + + mainSession.settings.viewportMode = GeckoSessionSettings.VIEWPORT_MODE_MOBILE + + mainSession.reload() + mainSession.waitForPageStop() + + innerWidth = mainSession.evaluateJS(innerWidthJs) as Double + assertThat( + "innerWidth should be equal to $mobileInnerWidth again", + innerWidth, + closeTo(mobileInnerWidth, 0.1), + ) + } + + @Test fun load() { + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("Session should not be null", session, notNullValue()) + assertThat("URI should not be null", request.uri, notNullValue()) + assertThat("URI should match", request.uri, endsWith(HELLO_HTML_PATH)) + assertThat( + "Trigger URL should be null", + request.triggerUri, + nullValue(), + ) + assertThat( + "App requested this load", + request.isDirectNavigation, + equalTo(true), + ) + assertThat("Target should not be null", request.target, notNullValue()) + assertThat( + "Target should match", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_CURRENT), + ) + assertThat("Redirect flag is not set", request.isRedirect, equalTo(false)) + assertThat("Should not have a user gesture", request.hasUserGesture, equalTo(false)) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("URL should not be null", url, notNullValue()) + assertThat("URL should match", url, endsWith(HELLO_HTML_PATH)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go back", canGoBack, equalTo(false)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + + @AssertCalled(false) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + }) + } + + @Test fun load_dataUri() { + val dataUrl = "data:,Hello%2C%20World!" + mainSession.loadUri(dataUrl) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match the provided data URL", url, equalTo(dataUrl)) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + @NullDelegate(NavigationDelegate::class) + @Test + fun load_withoutNavigationDelegate() { + // Test that when navigation delegate is disabled, we can still perform loads. + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.reload() + mainSession.waitForPageStop() + } + + @NullDelegate(NavigationDelegate::class) + @Test + fun load_canUnsetNavigationDelegate() { + // Test that if we unset the navigation delegate during a load, the load still proceeds. + var onLocationCount = 0 + mainSession.navigationDelegate = object : NavigationDelegate { + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + onLocationCount++ + } + } + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + + assertThat( + "Should get callback for first load", + onLocationCount, + equalTo(1), + ) + + mainSession.reload() + mainSession.navigationDelegate = null + mainSession.waitForPageStop() + + assertThat( + "Should not get callback for second load", + onLocationCount, + equalTo(1), + ) + } + + @Test fun loadString() { + val dataString = "<html><head><title>TheTitle</title></head><body>TheBody</body></html>" + val mimeType = "text/html" + mainSession.load(Loader().data(dataString, mimeType)) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate, ProgressDelegate, ContentDelegate { + @AssertCalled + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should match", title, equalTo("TheTitle")) + } + + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat( + "URL should be a data URL", + url, + equalTo(createDataUri(dataString, mimeType)), + ) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + @Test fun loadString_noMimeType() { + mainSession.load(Loader().data("Hello, World!", null)) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should be a data URL", url, startsWith("data:")) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + @Test fun loadData_html() { + val bytes = getTestBytes(HELLO_HTML_PATH) + assertThat("test html should have data", bytes.size, greaterThan(0)) + + mainSession.load(Loader().data(bytes, "text/html")) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate, ProgressDelegate, ContentDelegate { + @AssertCalled(count = 1) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should match", title, equalTo("Hello, world!")) + } + + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, equalTo(createDataUri(bytes, "text/html"))) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + private fun createDataUri( + data: String, + mimeType: String?, + ): String { + return String.format("data:%s,%s", mimeType ?: "", data) + } + + private fun createDataUri( + bytes: ByteArray, + mimeType: String?, + ): String { + return String.format( + "data:%s;base64,%s", + mimeType ?: "", + Base64.encodeToString(bytes, Base64.NO_WRAP), + ) + } + + fun loadDataHelper(assetPath: String, mimeType: String? = null) { + val bytes = getTestBytes(assetPath) + assertThat("test data should have bytes", bytes.size, greaterThan(0)) + + mainSession.load(Loader().data(bytes, mimeType)) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate, ProgressDelegate { + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, equalTo(createDataUri(bytes, mimeType))) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page should load successfully", success, equalTo(true)) + } + }) + } + + @Test fun loadData() { + loadDataHelper("/assets/www/images/test.gif", "image/gif") + } + + @Test fun loadData_noMimeType() { + loadDataHelper("/assets/www/images/test.gif") + } + + @Test fun reload() { + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + sessionRule.waitForPageStop() + + mainSession.reload() + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("URI should match", request.uri, endsWith(HELLO_HTML_PATH)) + assertThat( + "Trigger URL should be null", + request.triggerUri, + nullValue(), + ) + assertThat( + "Target should match", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_CURRENT), + ) + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, endsWith(HELLO_HTML_PATH)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Cannot go back", canGoBack, equalTo(false)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + + @AssertCalled(false) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + }) + } + + @Test fun goBackAndForward() { + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + sessionRule.waitForPageStop() + + mainSession.loadUri("$TEST_ENDPOINT$HELLO2_HTML_PATH") + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH)) + } + }) + + mainSession.goBack() + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 0, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, endsWith(HELLO_HTML_PATH)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Cannot go back", canGoBack, equalTo(false)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Can go forward", canGoForward, equalTo(true)) + } + + @AssertCalled(false) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + }) + + mainSession.goForward() + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 0, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Can go back", canGoBack, equalTo(true)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + + @AssertCalled(false) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + }) + } + + @Test fun onLoadUri_returnTrueCancelsLoad() { + sessionRule.delegateDuringNextWait(object : NavigationDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + if (request.uri.endsWith(HELLO_HTML_PATH)) { + return GeckoResult.deny() + } else { + return GeckoResult.allow() + } + } + }) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.loadTestPath(HELLO2_HTML_PATH) + sessionRule.waitForPageStop() + + sessionRule.forCallbacksDuringWait(object : ProgressDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH)) + } + + @AssertCalled(count = 1, order = [2]) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should succeed", success, equalTo(true)) + } + }) + } + + @Test fun onNewSession_calledForWindowOpen() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("window.open('newSession_child.html', '_blank')") + + mainSession.waitUntilCalled(object : NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("URI should be correct", request.uri, endsWith(NEW_SESSION_CHILD_HTML_PATH)) + assertThat( + "Trigger URL should match", + request.triggerUri, + endsWith(NEW_SESSION_HTML_PATH), + ) + assertThat( + "Target should be correct", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_NEW), + ) + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + assertThat("URI should be correct", uri, endsWith(NEW_SESSION_CHILD_HTML_PATH)) + return null + } + }) + } + + @Test(expected = GeckoSessionTestRule.RejectedPromiseException::class) + fun onNewSession_rejectLocal() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("window.open('file:///data/local/tmp', '_blank')") + } + + @Test fun onNewSession_calledForTargetBlankLink() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.querySelector('#targetBlankLink').click()") + + mainSession.waitUntilCalled(object : NavigationDelegate { + // We get two onLoadRequest calls for the link click, + // one when loading the URL and one when opening a new window. + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat("URI should be correct", request.uri, endsWith(NEW_SESSION_CHILD_HTML_PATH)) + assertThat( + "Trigger URL should be null", + request.triggerUri, + endsWith(NEW_SESSION_HTML_PATH), + ) + assertThat( + "Target should be correct", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_NEW), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + assertThat("URI should be correct", uri, endsWith(NEW_SESSION_CHILD_HTML_PATH)) + return null + } + }) + } + + private fun delegateNewSession(settings: GeckoSessionSettings = mainSession.settings): GeckoSession { + val newSession = sessionRule.createClosedSession(settings) + + mainSession.delegateDuringNextWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession> { + return GeckoResult.fromValue(newSession) + } + }) + + return newSession + } + + @Test fun onNewSession_childShouldLoad() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + val newSession = delegateNewSession() + mainSession.evaluateJS("document.querySelector('#targetBlankLink').click()") + // Initial about:blank + newSession.waitForPageStop() + // NEW_SESSION_CHILD_HTML_PATH + newSession.waitForPageStop() + + newSession.forCallbacksDuringWait(object : ProgressDelegate { + @AssertCalled(count = 1) + override fun onPageStart(session: GeckoSession, url: String) { + assertThat("URL should match", url, endsWith(NEW_SESSION_CHILD_HTML_PATH)) + } + + @AssertCalled(count = 1) + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Load should succeed", success, equalTo(true)) + } + }) + } + + @Test fun onNewSession_setWindowOpener() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + val newSession = delegateNewSession() + mainSession.evaluateJS("document.querySelector('#targetBlankLink').click()") + newSession.waitForPageStop() + + assertThat( + "window.opener should be set", + newSession.evaluateJS("window.opener.location.pathname") as String, + equalTo(NEW_SESSION_HTML_PATH), + ) + } + + @Test fun onNewSession_supportNoOpener() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + val newSession = delegateNewSession() + mainSession.evaluateJS("document.querySelector('#noOpenerLink').click()") + newSession.waitForPageStop() + + assertThat( + "window.opener should not be set", + newSession.evaluateJS("window.opener"), + equalTo(JSONObject.NULL), + ) + } + + @Test fun onNewSession_notCalledForHandledLoads() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.delegateDuringNextWait(object : NavigationDelegate { + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + // Pretend we handled the target="_blank" link click. + if (request.uri.endsWith(NEW_SESSION_CHILD_HTML_PATH)) { + return GeckoResult.deny() + } else { + return GeckoResult.allow() + } + } + }) + + mainSession.evaluateJS("document.querySelector('#targetBlankLink').click()") + + mainSession.reload() + mainSession.waitForPageStop() + + // Assert that onNewSession was not called for the link click. + mainSession.forCallbacksDuringWait(object : NavigationDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "URI must match", + request.uri, + endsWith(forEachCall(NEW_SESSION_CHILD_HTML_PATH, NEW_SESSION_HTML_PATH)), + ) + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 0) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? { + return null + } + }) + } + + @Test fun onNewSession_submitFormWithTargetBlank() { + mainSession.loadTestPath(FORM_BLANK_HTML_PATH) + sessionRule.waitForPageStop() + + mainSession.evaluateJS( + """ + document.querySelector('input[type=text]').focus() + """, + ) + mainSession.waitUntilCalled( + TextInputDelegate::class, + "restartInput", + ) + + val time = SystemClock.uptimeMillis() + val keyEvent = KeyEvent(time, time, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0) + mainSession.textInput.onKeyDown(KeyEvent.KEYCODE_ENTER, keyEvent) + mainSession.textInput.onKeyUp( + KeyEvent.KEYCODE_ENTER, + KeyEvent.changeAction( + keyEvent, + KeyEvent.ACTION_UP, + ), + ) + + mainSession.waitUntilCalled(object : NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): + GeckoResult<AllowOrDeny>? { + assertThat( + "URL should be correct", + request.uri, + endsWith("form_blank.html?"), + ) + assertThat( + "Trigger URL should match", + request.triggerUri, + endsWith("form_blank.html"), + ) + assertThat( + "Target should be correct", + request.target, + equalTo(NavigationDelegate.TARGET_WINDOW_NEW), + ) + return null + } + + @AssertCalled(count = 1, order = [2]) + override fun onNewSession(session: GeckoSession, uri: String): + GeckoResult<GeckoSession>? { + assertThat("URL should be correct", uri, endsWith("form_blank.html?")) + return null + } + }) + } + + @Test fun loadUriReferrer() { + val uri = "https://example.com" + val referrer = "https://foo.org/" + + mainSession.load( + Loader() + .uri(uri) + .referrer(referrer) + .flags(GeckoSession.LOAD_FLAGS_NONE), + ) + mainSession.waitForPageStop() + + assertThat( + "Referrer should match", + mainSession.evaluateJS("document.referrer") as String, + equalTo(referrer), + ) + } + + @Test fun loadUriReferrerSession() { + val uri = "https://example.com/bar" + val referrer = "https://example.org/" + + mainSession.loadUri(referrer) + mainSession.waitForPageStop() + + val newSession = sessionRule.createOpenSession() + newSession.load( + Loader() + .uri(uri) + .referrer(mainSession) + .flags(GeckoSession.LOAD_FLAGS_NONE), + ) + newSession.waitForPageStop() + + assertThat( + "Referrer should match", + newSession.evaluateJS("document.referrer") as String, + equalTo(referrer), + ) + } + + @Test fun loadUriReferrerSessionFileUrl() { + val uri = "file:///system/etc/fonts.xml" + val referrer = "https://example.org" + + mainSession.loadUri(referrer) + mainSession.waitForPageStop() + + val newSession = sessionRule.createOpenSession() + newSession.load( + Loader() + .uri(uri) + .referrer(mainSession) + .flags(GeckoSession.LOAD_FLAGS_NONE), + ) + newSession.waitUntilCalled(object : NavigationDelegate { + @AssertCalled + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + return null + } + }) + } + + private fun loadUriHeaderTest( + headers: Map<String?, String?>, + additional: Map<String?, String?>, + filter: Int = GeckoSession.HEADER_FILTER_CORS_SAFELISTED, + ) { + // First collect default headers with no override + mainSession.loadUri("$TEST_ENDPOINT/anything") + mainSession.waitForPageStop() + + val defaultContent = mainSession.evaluateJS("document.body.children[0].innerHTML") as String + val defaultBody = JSONObject(defaultContent) + val defaultHeaders = defaultBody.getJSONObject("headers").asMap<String>() + + val expected = HashMap(additional) + for (key in defaultHeaders.keys) { + expected[key] = defaultHeaders[key] + if (additional.containsKey(key)) { + // TODO: Bug 1671294, headers should be replaced, not appended + expected[key] += ", " + additional[key] + } + } + + // Now load the page with the header override + mainSession.load( + Loader() + .uri("$TEST_ENDPOINT/anything") + .additionalHeaders(headers) + .headerFilter(filter), + ) + mainSession.waitForPageStop() + + val content = mainSession.evaluateJS("document.body.children[0].innerHTML") as String + val body = JSONObject(content) + val actualHeaders = body.getJSONObject("headers").asMap<String>() + + assertThat( + "Headers should match", + expected as Map<String?, String?>, + equalTo(actualHeaders), + ) + } + + private fun testLoaderEquals(a: Loader, b: Loader, shouldBeEqual: Boolean) { + assertThat("Equal test", a == b, equalTo(shouldBeEqual)) + assertThat( + "HashCode test", + a.hashCode() == b.hashCode(), + equalTo(shouldBeEqual), + ) + } + + @Test fun loaderEquals() { + testLoaderEquals( + Loader().uri("http://test-uri-equals.com"), + Loader().uri("http://test-uri-equals.com"), + true, + ) + testLoaderEquals( + Loader().uri("http://test-uri-equals.com"), + Loader().uri("http://test-uri-equalsx.com"), + false, + ) + + testLoaderEquals( + Loader().uri("http://test-uri-equals.com") + .flags(GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER) + .headerFilter(GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE) + .referrer("test-referrer"), + Loader().uri("http://test-uri-equals.com") + .flags(GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER) + .headerFilter(GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE) + .referrer("test-referrer"), + true, + ) + testLoaderEquals( + Loader().uri("http://test-uri-equals.com") + .flags(GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER) + .headerFilter(GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE) + .referrer(mainSession), + Loader().uri("http://test-uri-equals.com") + .flags(GeckoSession.LOAD_FLAGS_BYPASS_CLASSIFIER) + .headerFilter(GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE) + .referrer("test-referrer"), + false, + ) + + testLoaderEquals( + Loader().referrer(mainSession) + .data("testtest", "text/plain"), + Loader().referrer(mainSession) + .data("testtest", "text/plain"), + true, + ) + testLoaderEquals( + Loader().referrer(mainSession) + .data("testtest", "text/plain"), + Loader().referrer("test-referrer") + .data("testtest", "text/plain"), + false, + ) + } + + @Test fun loadUriHeader() { + // Basic test + loadUriHeaderTest( + mapOf("Header1" to "Value", "Header2" to "Value1, Value2"), + mapOf(), + ) + loadUriHeaderTest( + mapOf("Header1" to "Value", "Header2" to "Value1, Value2"), + mapOf("Header1" to "Value", "Header2" to "Value1, Value2"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + // Empty value headers are ignored + loadUriHeaderTest( + mapOf("ValueLess1" to "", "ValueLess2" to null), + mapOf(), + ) + + // Null key or special headers are ignored + loadUriHeaderTest( + mapOf( + null to "BadNull", + "Connection" to "BadConnection", + "Host" to "BadHost", + ), + mapOf(), + ) + + // Key or value cannot contain '\r\n' + loadUriHeaderTest( + mapOf( + "Header1" to "Value", + "Header2" to "Value1, Value2", + "this\r\nis invalid" to "test value", + "test key" to "this\r\n is a no-no", + "what" to "what\r\nhost:amazon.com", + "Header3" to "Value1, Value2, Value3", + ), + mapOf(), + ) + loadUriHeaderTest( + mapOf( + "Header1" to "Value", + "Header2" to "Value1, Value2", + "this\r\nis invalid" to "test value", + "test key" to "this\r\n is a no-no", + "what" to "what\r\nhost:amazon.com", + "Header3" to "Value1, Value2, Value3", + ), + mapOf( + "Header1" to "Value", + "Header2" to "Value1, Value2", + "Header3" to "Value1, Value2, Value3", + ), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + loadUriHeaderTest( + mapOf( + "Header1" to "Value", + "Header2" to "Value1, Value2", + "what" to "what\r\nhost:amazon.com", + ), + mapOf(), + ) + loadUriHeaderTest( + mapOf( + "Header1" to "Value", + "Header2" to "Value1, Value2", + "what" to "what\r\nhost:amazon.com", + ), + mapOf("Header1" to "Value", "Header2" to "Value1, Value2"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + loadUriHeaderTest( + mapOf("what" to "what\r\nhost:amazon.com"), + mapOf(), + ) + + loadUriHeaderTest( + mapOf("this\r\n" to "yes"), + mapOf(), + ) + + // Connection and Host cannot be overriden, no matter the case spelling + loadUriHeaderTest( + mapOf("Header1" to "Value1", "ConnEction" to "test", "connection" to "test2"), + mapOf(), + ) + loadUriHeaderTest( + mapOf("Header1" to "Value1", "ConnEction" to "test", "connection" to "test2"), + mapOf("Header1" to "Value1"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + loadUriHeaderTest( + mapOf("Header1" to "Value1", "connection" to "test2"), + mapOf(), + ) + loadUriHeaderTest( + mapOf("Header1" to "Value1", "connection" to "test2"), + mapOf("Header1" to "Value1"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + loadUriHeaderTest( + mapOf("Header1 " to "Value1", "host" to "test2"), + mapOf(), + ) + loadUriHeaderTest( + mapOf("Header1 " to "Value1", "host" to "test2"), + mapOf("Header1" to "Value1"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + loadUriHeaderTest( + mapOf("Header1" to "Value1", "host" to "test2"), + mapOf(), + ) + loadUriHeaderTest( + mapOf("Header1" to "Value1", "host" to "test2"), + mapOf("Header1" to "Value1"), + GeckoSession.HEADER_FILTER_UNRESTRICTED_UNSAFE, + ) + + // Adding white space at the end of a forbidden header still prevents override + loadUriHeaderTest( + mapOf( + "host" to "amazon.com", + "host " to "amazon.com", + "host\r" to "amazon.com", + "host\r\n" to "amazon.com", + ), + mapOf(), + ) + + // '\r' or '\n' are forbidden character even when not following each other + loadUriHeaderTest( + mapOf("abc\ra\n" to "amazon.com"), + mapOf(), + ) + + // CORS Safelist test + loadUriHeaderTest( + mapOf( + "Accept-Language" to "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", + "Accept" to "text/html", + "Content-Language" to "de-DE, en-CA", + "Content-Type" to "multipart/form-data; boundary=something", + ), + mapOf( + "Accept-Language" to "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", + "Accept" to "text/html", + "Content-Language" to "de-DE, en-CA", + "Content-Type" to "multipart/form-data; boundary=something", + ), + GeckoSession.HEADER_FILTER_CORS_SAFELISTED, + ) + + // CORS safelist doesn't allow Content-type image/svg + loadUriHeaderTest( + mapOf( + "Accept-Language" to "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", + "Accept" to "text/html", + "Content-Language" to "de-DE, en-CA", + "Content-Type" to "image/svg; boundary=something", + ), + mapOf( + "Accept-Language" to "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", + "Accept" to "text/html", + "Content-Language" to "de-DE, en-CA", + ), + GeckoSession.HEADER_FILTER_CORS_SAFELISTED, + ) + } + + @Test(expected = GeckoResult.UncaughtException::class) + fun onNewSession_doesNotAllowOpened() { + // Disable popup blocker. + sessionRule.setPrefsUntilTestEnd(mapOf("dom.disable_open_during_load" to false)) + + mainSession.loadTestPath(NEW_SESSION_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.delegateDuringNextWait(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession> { + return GeckoResult.fromValue(sessionRule.createOpenSession()) + } + }) + + mainSession.evaluateJS("document.querySelector('#targetBlankLink').click()") + + mainSession.waitUntilCalled( + NavigationDelegate::class, + "onNewSession", + ) + UiThreadUtils.loopUntilIdle(sessionRule.env.defaultTimeoutMillis) + } + + @Test + fun extensionProcessSwitching() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "xpinstall.signatures.required" to false, + "extensions.install.requireBuiltInCerts" to false, + "extensions.update.requireBuiltInCerts" to false, + ), + ) + + val controller = sessionRule.runtime.webExtensionController + + sessionRule.delegateUntilTestEnd(object : WebExtensionController.PromptDelegate { + @AssertCalled + override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> { + return GeckoResult.allow() + } + }) + + val extension = sessionRule.waitForResult( + controller.install("https://example.org/tests/junit/page-history.xpi"), + ) + + assertThat( + "baseUrl should be a valid extension URL", + extension.metaData.baseUrl, + startsWith("moz-extension://"), + ) + + val url = extension.metaData.baseUrl + "page.html" + processSwitchingTest(url) + + sessionRule.waitForResult(controller.uninstall(extension)) + } + + @Test + fun mainProcessSwitching() { + processSwitchingTest("about:config") + } + + private fun processSwitchingTest(url: String) { + val settings = sessionRule.runtime.settings + val aboutConfigEnabled = settings.aboutConfigEnabled + settings.aboutConfigEnabled = true + + var currentUrl: String? = null + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + currentUrl = url + } + + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + assertThat("Should not get here", false, equalTo(true)) + return null + } + }) + + // This will load a page in the child + mainSession.loadTestPath(HELLO2_HTML_PATH) + sessionRule.waitForPageStop() + + assertThat( + "docShell should start out active", + mainSession.active, + equalTo(true), + ) + + // This loads in the parent process + mainSession.loadUri(url) + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, equalTo(url)) + + // This will load a page in the child + mainSession.loadTestPath(HELLO_HTML_PATH) + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, endsWith(HELLO_HTML_PATH)) + assertThat( + "docShell should be active after switching process", + mainSession.active, + equalTo(true), + ) + + mainSession.loadUri(url) + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, equalTo(url)) + + mainSession.goBack() + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, endsWith(HELLO_HTML_PATH)) + assertThat( + "docShell should be active after switching process", + mainSession.active, + equalTo(true), + ) + + mainSession.goBack() + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, equalTo(url)) + + mainSession.goBack() + sessionRule.waitForPageStop() + + assertThat("URL should match", currentUrl!!, endsWith(HELLO2_HTML_PATH)) + assertThat( + "docShell should be active after switching process", + mainSession.active, + equalTo(true), + ) + + settings.aboutConfigEnabled = aboutConfigEnabled + } + + @Test fun setLocationHash() { + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + sessionRule.waitForPageStop() + + mainSession.evaluateJS("location.hash = 'test1';") + + mainSession.waitUntilCalled(object : NavigationDelegate { + @AssertCalled(count = 0) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return null + } + + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URI should match", url, endsWith("#test1")) + } + }) + + mainSession.evaluateJS("location.hash = 'test2';") + + mainSession.waitUntilCalled(object : NavigationDelegate { + @AssertCalled(count = 0) + override fun onLoadRequest( + session: GeckoSession, + request: LoadRequest, + ): + GeckoResult<AllowOrDeny>? { + return null + } + + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URI should match", url, endsWith("#test2")) + } + }) + } + + @Test fun purgeHistory() { + // TODO: Bug 1648158 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + sessionRule.waitUntilCalled(object : HistoryDelegate, NavigationDelegate { + @AssertCalled(count = 1) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go back", canGoBack, equalTo(false)) + } + + @AssertCalled(count = 1) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + + @AssertCalled(count = 1) + override fun onHistoryStateChange(session: GeckoSession, state: HistoryDelegate.HistoryList) { + assertThat("History should have one entry", state.size, equalTo(1)) + } + }) + mainSession.loadUri("$TEST_ENDPOINT$HELLO2_HTML_PATH") + sessionRule.waitUntilCalled(object : HistoryDelegate, NavigationDelegate { + @AssertCalled(count = 1) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go back", canGoBack, equalTo(true)) + } + + @AssertCalled(count = 1) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + + @AssertCalled(count = 1) + override fun onHistoryStateChange(session: GeckoSession, state: HistoryDelegate.HistoryList) { + assertThat("History should have two entries", state.size, equalTo(2)) + } + }) + mainSession.purgeHistory() + sessionRule.waitUntilCalled(object : HistoryDelegate, NavigationDelegate { + @AssertCalled(count = 1) + override fun onHistoryStateChange(session: GeckoSession, state: HistoryDelegate.HistoryList) { + assertThat("History should have one entry", state.size, equalTo(1)) + } + + @AssertCalled(count = 1) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go back", canGoBack, equalTo(false)) + } + + @AssertCalled(count = 1) + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + assertThat("Session should not be null", session, notNullValue()) + assertThat("Cannot go forward", canGoForward, equalTo(false)) + } + }) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun userGesture() { + mainSession.loadUri("$TEST_ENDPOINT$CLICK_TO_RELOAD_HTML_PATH") + mainSession.waitForPageStop() + + mainSession.synthesizeTap(50, 50) + + sessionRule.waitUntilCalled(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + assertThat("Should have a user gesture", request.hasUserGesture, equalTo(true)) + assertThat( + "Load should not be direct", + request.isDirectNavigation, + equalTo(false), + ) + return GeckoResult.allow() + } + }) + } + + @Test fun loadAfterLoad() { + mainSession.delegateDuringNextWait(object : NavigationDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + assertThat("URLs should match", request.uri, endsWith(forEachCall(HELLO_HTML_PATH, HELLO2_HTML_PATH))) + return GeckoResult.allow() + } + }) + + mainSession.loadUri("$TEST_ENDPOINT$HELLO_HTML_PATH") + mainSession.loadUri("$TEST_ENDPOINT$HELLO2_HTML_PATH") + mainSession.waitForPageStop() + } + + @Test + fun loadLongDataUriToplevelDirect() { + val dataBytes = ByteArray(3 * 1024 * 1024) + val expectedUri = createDataUri(dataBytes, "*/*") + val loader = Loader().data(dataBytes, "*/*") + + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + assertThat("URLs should match", request.uri, equalTo(expectedUri)) + return GeckoResult.allow() + } + + @AssertCalled(count = 1, order = [2]) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + assertThat( + "Error category should match", + error.category, + equalTo(WebRequestError.ERROR_CATEGORY_URI), + ) + assertThat( + "Error code should match", + error.code, + equalTo(WebRequestError.ERROR_DATA_URI_TOO_LONG), + ) + assertThat("URLs should match", uri, equalTo(expectedUri)) + return null + } + }) + + mainSession.load(loader) + sessionRule.waitUntilCalled(NavigationDelegate::class, "onLoadError") + } + + @Test + fun loadLongDataUriToplevelIndirect() { + val dataBytes = ByteArray(3 * 1024 * 1024) + val dataUri = createDataUri(dataBytes, "*/*") + + mainSession.loadTestPath(DATA_URI_PATH) + mainSession.waitForPageStop() + + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return GeckoResult.deny() + } + }) + + mainSession.evaluateJS("document.querySelector('#largeLink').href = \"$dataUri\"") + mainSession.evaluateJS("document.querySelector('#largeLink').click()") + mainSession.waitForPageStop() + } + + @Test + @NullDelegate(NavigationDelegate::class) + fun loadOnBackgroundThreadNullNavigationDelegate() { + thread { + // Make sure we're running in a thread without a Looper. + assertThat( + "We should not have a looper.", + Looper.myLooper(), + equalTo(null), + ) + mainSession.loadTestPath(HELLO_HTML_PATH) + } + + mainSession.waitUntilCalled(object : ProgressDelegate { + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page loaded successfully", success, equalTo(true)) + } + }) + } + + @Test + fun invalidScheme() { + val invalidUri = "tel:#12345678" + mainSession.loadUri(invalidUri) + mainSession.waitUntilCalled(object : NavigationDelegate { + override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? { + assertThat("Uri should match", uri, equalTo(invalidUri)) + assertThat( + "error should match", + error.code, + equalTo(WebRequestError.ERROR_MALFORMED_URI), + ) + assertThat( + "error should match", + error.category, + equalTo(WebRequestError.ERROR_CATEGORY_URI), + ) + return null + } + }) + } + + @Test + fun loadOnBackgroundThread() { + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return GeckoResult.allow() + } + }) + + thread { + // Make sure we're running in a thread without a Looper. + assertThat( + "We should not have a looper.", + Looper.myLooper(), + equalTo(null), + ) + mainSession.loadTestPath(HELLO_HTML_PATH) + } + + mainSession.waitUntilCalled(object : ProgressDelegate { + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page loaded successfully", success, equalTo(true)) + } + }) + } + + @Test + fun loadShortDataUriToplevelIndirect() { + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + @AssertCalled(count = 2) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return GeckoResult.allow() + } + + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }) + + val dataBytes = this.getTestBytes("/assets/www/images/test.gif") + val uri = createDataUri(dataBytes, "image/*") + + mainSession.loadTestPath(DATA_URI_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.querySelector('#smallLink').href = \"$uri\"") + mainSession.evaluateJS("document.querySelector('#smallLink').click()") + mainSession.waitForPageStop() + } + + fun createLargeHighEntropyImageDataUri(): String { + val desiredMinSize = (2 * 1024 * 1024) + 1 + + val width = 768 + val height = 768 + + val bitmap = Bitmap.createBitmap( + ThreadLocalRandom.current().ints(width.toLong() * height.toLong()).toArray(), + width, + height, + Bitmap.Config.ARGB_8888, + ) + + val stream = ByteArrayOutputStream() + if (!bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)) { + throw Exception("Error compressing PNG") + } + + val uri = createDataUri(stream.toByteArray(), "image/png") + + if (uri.length < desiredMinSize) { + throw Exception("Test uri is too small, want at least " + desiredMinSize + ", got " + uri.length) + } + + return uri + } + + @Test + fun loadLongDataUriNonToplevel() { + val dataUri = createLargeHighEntropyImageDataUri() + + mainSession.delegateUntilTestEnd(object : NavigationDelegate { + @AssertCalled(count = 1) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return GeckoResult.allow() + } + + @AssertCalled(false) + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError, + ): GeckoResult<String>? { + return null + } + }) + + mainSession.loadTestPath(DATA_URI_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.querySelector('#image').onload = () => { imageLoaded = true; }") + mainSession.evaluateJS("document.querySelector('#image').src = \"$dataUri\"") + UiThreadUtils.waitForCondition({ + mainSession.evaluateJS("document.querySelector('#image').complete") as Boolean + }, sessionRule.env.defaultTimeoutMillis) + mainSession.evaluateJS("if (!imageLoaded) throw imageLoaded") + } + + @Test + fun bypassLoadUriDelegate() { + val testUri = "https://www.mozilla.org" + + mainSession.load( + Loader() + .uri(testUri) + .flags(GeckoSession.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE), + ) + mainSession.waitForPageStop() + + sessionRule.forCallbacksDuringWait( + object : NavigationDelegate { + @AssertCalled(false) + override fun onLoadRequest(session: GeckoSession, request: LoadRequest): GeckoResult<AllowOrDeny>? { + return null + } + }, + ) + } + + @Test fun goBackFromHistory() { + // TODO: Bug 1673954 + assumeThat(sessionRule.env.isFission, equalTo(false)) + + mainSession.loadTestPath(HELLO_HTML_PATH) + + mainSession.waitUntilCalled(object : HistoryDelegate, ContentDelegate { + @AssertCalled(count = 1) + override fun onHistoryStateChange(session: GeckoSession, state: HistoryDelegate.HistoryList) { + assertThat("History should have one entry", state.size, equalTo(1)) + } + + @AssertCalled(count = 1) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should match", title, equalTo("Hello, world!")) + } + }) + + mainSession.loadTestPath(HELLO2_HTML_PATH) + + mainSession.waitUntilCalled(object : HistoryDelegate, NavigationDelegate, ContentDelegate { + @AssertCalled(count = 1) + override fun onHistoryStateChange(session: GeckoSession, state: HistoryDelegate.HistoryList) { + assertThat("History should have two entry", state.size, equalTo(2)) + } + + @AssertCalled(count = 1) + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + assertThat("Can go back", canGoBack, equalTo(true)) + } + + @AssertCalled(count = 1) + override fun onTitleChange(session: GeckoSession, title: String?) { + assertThat("Title should match", title, equalTo("Hello, world! Again!")) + } + }) + + // goBack will be navigated from history. + + var lastTitle: String? = "" + sessionRule.delegateDuringNextWait(object : NavigationDelegate, ContentDelegate { + @AssertCalled(count = 1) + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList<PermissionDelegate.ContentPermission>, + ) { + assertThat("URL should match", url, endsWith(HELLO_HTML_PATH)) + } + + @AssertCalled + override fun onTitleChange(session: GeckoSession, title: String?) { + lastTitle = title + } + }) + + mainSession.goBack() + sessionRule.waitForPageStop() + assertThat("Title should match", lastTitle, equalTo("Hello, world!")) + } + + @Test + fun loadAndroidAssets() { + val assetUri = "resource://android/assets/web_extensions/" + mainSession.loadUri(assetUri) + + mainSession.waitUntilCalled(object : ProgressDelegate { + override fun onPageStop(session: GeckoSession, success: Boolean) { + assertThat("Page loaded successfully", success, equalTo(true)) + } + }) + } +} |