chromium/chrome/android/javatests/src/org/chromium/chrome/browser/UrlSchemeTest.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser;

import android.content.Context;
import android.net.Uri;
import android.os.Environment;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.TestFileUtil;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.TestContentProvider;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.net.test.EmbeddedTestServer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;

/** Test suite for different Android URL schemes. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class UrlSchemeTest {
    @ClassRule
    public static final ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public final BlankCTATabInitialStateRule mInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, false);

    private static final String SIMPLE_SRC = "simple.html";
    private static final String SIMPLE_IMAGE = "google.png";

    private EmbeddedTestServer mTestServer;

    @Before
    public void setUp() {
        TestContentProvider.resetResourceRequestCounts(ApplicationProvider.getApplicationContext());
        TestContentProvider.setDataFilePath(
                ApplicationProvider.getApplicationContext(), UrlUtils.getTestFilePath(""));
        mTestServer =
                EmbeddedTestServer.createAndStartServer(
                        ApplicationProvider.getApplicationContext());
    }

    /**
     * Test that resource request count in the content provider works. This is to make sure that
     * attempts to access the content provider will be detected.
     */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentProviderResourceRequestCount() throws IOException {
        String resource = SIMPLE_SRC;
        ensureResourceRequestCountInContentProvider(resource, 0);
        // Make a request to the content provider.
        Uri uri = Uri.parse(createContentUrl(resource));
        Context context = ApplicationProvider.getApplicationContext();
        InputStream inputStream = null;
        try {
            inputStream = context.getContentResolver().openInputStream(uri);
            Assert.assertNotNull(inputStream);
        } finally {
            if (inputStream != null) inputStream.close();
        }
        ensureResourceRequestCountInContentProvider(resource, 1);
    }

    /** Make sure content URL access works. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlAccess() {
        String resource = SIMPLE_SRC;
        sActivityTestRule.loadUrl(createContentUrl(resource));
        ensureResourceRequestCountInContentProviderNotLessThan(resource, 1);
    }

    /**
     * Make sure a Content url *CANNOT* access the contents of an iframe that is loaded as a content
     * URL.
     */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlIframeAccessFromContentUrl() throws Throwable {
        final String resource = "page_with_iframe_as_content_url.html";
        final String iframe = "simple_iframe.html";
        final String iframeId = "iframe_test_id";

        final String script =
                "var ifrm = document.getElementById('"
                        + iframeId
                        + "');"
                        + "try {"
                        + "  var a = ifrm.contentWindow.document.body.textContent;"
                        + "} catch (e) {"
                        + "  document.title = 'fail';"
                        + "}";

        sActivityTestRule.loadUrl(createContentUrl(resource));

        // Make sure iframe is really loaded by verifying the title
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getTitleOnUiThread(
                                    sActivityTestRule.getActivity().getActivityTab()),
                            Matchers.is("iframe loaded"));
                });
        // Make sure that content provider was asked to provide the content.
        ensureResourceRequestCountInContentProviderNotLessThan(iframe, 1);
        sActivityTestRule.runJavaScriptCodeInCurrentTab(script);

        // Make sure content access failed by verifying that title is set to fail.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getTitleOnUiThread(
                                    sActivityTestRule.getActivity().getActivityTab()),
                            Matchers.is("fail"));
                });
    }

    private String loadContentUrlToMakeCorsToContent(final String api, final String mode)
            throws Throwable {
        final String resource = "content_url_make_cors_to_content.html";
        final String imageUrl = createContentUrl("google.png");

        sActivityTestRule.loadUrl(
                createContentUrl(resource)
                        + "?api="
                        + api
                        + "&mode="
                        + mode
                        + "&url="
                        + URLEncoder.encode(imageUrl));

        // Make sure the CORS request fail in the page.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getTitleOnUiThread(
                                    sActivityTestRule.getActivity().getActivityTab()),
                            Matchers.not("running"));
                });

        // Make sure that content provider was asked to provide the content.
        ensureResourceRequestCountInContentProviderNotLessThan(resource, 1);

        return ChromeTabUtils.getTitleOnUiThread(sActivityTestRule.getActivity().getActivityTab());
    }

    @Test
    @MediumTest
    @Feature({"CORS"})
    public void testContentUrlCanNotMakeXhrRequestToContentUrl() throws Throwable {
        // The XMLHttpRequest can carry content:// URLs, but CORS requests for content:// are not
        // permitted.
        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("xhr", ""));
    }

    @Test
    @MediumTest
    @Feature({"CORS"})
    public void testContentUrlCanNotMakeFetchCorsRequestToContentUrl() throws Throwable {
        // The Fetch API does not support content:// scheme.
        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "cors"));
    }

    @Test
    @MediumTest
    @Feature({"CORS"})
    public void testContentUrlCanNotMakeFetchSameOriginRequestToContentUrl() throws Throwable {
        // The Fetch API does not support content:// scheme.
        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "same-origin"));
    }

    @Test
    @MediumTest
    @Feature({"CORS"})
    public void testContentUrlCanNotMakeFetchNoCorsRequestToContentUrl() throws Throwable {
        // The Fetch API does not support content:// scheme.
        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "no-cors"));
    }

    @Test
    @MediumTest
    @Feature({"CORS"})
    public void testContentUrlToLoadWorkerFromContent() throws Throwable {
        final String resource = "content_url_load_content_worker.html";

        sActivityTestRule.loadUrl(createContentUrl(resource));

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getTitleOnUiThread(
                                    sActivityTestRule.getActivity().getActivityTab()),
                            Matchers.not("running"));
                });

        // Make sure that content provider was asked to provide the content.
        ensureResourceRequestCountInContentProviderNotLessThan(resource, 1);

        Assert.assertEquals(
                "exception",
                ChromeTabUtils.getTitleOnUiThread(
                        sActivityTestRule.getActivity().getActivityTab()));
    }

    /** Test that a content URL is *ALLOWED* to access an image provided by a content URL. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlImageFromContentUrl() throws Throwable {
        verifyImageLoadRules(createContentUrl(SIMPLE_SRC), "success", 1);
    }

    /** Test that a HTTP URL is *NOT ALLOWED* to access an image provided by a content URL. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlImageFromHttpUrl() throws Throwable {
        final String main = mTestServer.getURL("/chrome/test/data/android/" + SIMPLE_SRC);
        verifyImageLoadRules(main, "error", 0);
    }

    private void verifyImageLoadRules(String url, final String expectedTitle, int expectedLoadCount)
            throws Throwable {
        final String resource = SIMPLE_IMAGE;
        final String script =
                "var img = new Image();"
                        + "  img.onerror = function() { document.title = 'error' };"
                        + "  img.onabort = function() { document.title = 'error' };"
                        + "  img.onload = function() { document.title = 'success' };"
                        + "  img.src = '"
                        + createContentUrl(resource)
                        + "';"
                        + "  document.body.appendChild(img);";
        sActivityTestRule.loadUrl(url);
        sActivityTestRule.runJavaScriptCodeInCurrentTab(script);

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeTabUtils.getTitleOnUiThread(
                                    sActivityTestRule.getActivity().getActivityTab()),
                            Matchers.is(expectedTitle));
                });
        ensureResourceRequestCountInContentProviderNotLessThan(resource, expectedLoadCount);
    }

    /** Test that a content URL is not allowed within a data URL. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlFromData() {
        final String target = SIMPLE_IMAGE;
        sActivityTestRule.loadUrl(
                UrlUtils.encodeHtmlDataUri("<img src=\"" + createContentUrl(target) + "\">"));
        ensureResourceRequestCountInContentProvider(target, 0);
    }

    /** Test that a content URL is not allowed within a local file. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testContentUrlFromFile() throws IOException {
        final String target = SIMPLE_IMAGE;
        final File file = new File(Environment.getExternalStorageDirectory(), target + ".html");
        try {
            TestFileUtil.createNewHtmlFile(
                    file, target, "<img src=\"" + createContentUrl(target) + "\">");
            sActivityTestRule.loadUrl("file://" + file.getAbsolutePath());
            ensureResourceRequestCountInContentProvider(target, 0);
        } finally {
            TestFileUtil.deleteFile(file);
        }
    }

    /** Test that the browser can be navigated to a file URL. */
    @Test
    @MediumTest
    @Feature({"Navigation"})
    public void testFileUrlNavigation() throws IOException {
        final File file =
                new File(Environment.getExternalStorageDirectory(), "url_navigation_test.html");

        try {
            TestFileUtil.createNewHtmlFile(file, "File", null);
            sActivityTestRule.loadUrl("file://" + file.getAbsolutePath());
            Assert.assertEquals(
                    "File",
                    ChromeTabUtils.getTitleOnUiThread(
                            sActivityTestRule.getActivity().getActivityTab()));
        } finally {
            TestFileUtil.deleteFile(file);
        }
    }

    /**
     * Verifies the number of resource requests made to the content provider.
     *
     * @param resource Resource name
     * @param expectedCount Expected resource requests count
     */
    private void ensureResourceRequestCountInContentProvider(String resource, int expectedCount) {
        Context context = ApplicationProvider.getApplicationContext();
        int actualCount = TestContentProvider.getResourceRequestCount(context, resource);
        Assert.assertEquals(expectedCount, actualCount);
    }

    /**
     * Verifies the number of resource requests made to the content provider.
     *
     * @param resource Resource name
     * @param expectedMinimalCount Expected minimal resource requests count
     */
    private void ensureResourceRequestCountInContentProviderNotLessThan(
            String resource, int expectedMinimalCount) {
        Context context = ApplicationProvider.getApplicationContext();
        int actualCount = TestContentProvider.getResourceRequestCount(context, resource);
        Assert.assertTrue(
                "Minimal expected: " + expectedMinimalCount + ", actual: " + actualCount,
                actualCount >= expectedMinimalCount);
    }

    private String createContentUrl(final String target) {
        return TestContentProvider.createContentUrl(target);
    }
}