chromium/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/TrustedCdnPublisherUrlTest.java

// Copyright 2018 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.customtabs;

import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.CoreMatchers.allOf;

import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.VectorDrawable;
import android.util.Pair;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.core.widget.ImageViewCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;

import org.chromium.base.CommandLine;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.AnnotationRule;
import org.chromium.base.test.util.CallbackHelper;
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.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.content.CustomTabIntentHandler;
import org.chromium.chrome.browser.customtabs.dependency_injection.BaseCustomTabActivityModule;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar.CustomTabLocationBar;
import org.chromium.chrome.browser.dependency_injection.ModuleOverridesRule;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.offlinepages.ClientId;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.omnibox.UrlBar;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TrustedCdn;
import org.chromium.chrome.browser.test.ScreenShooter;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.components.offlinepages.SavePageResult;
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.test.util.TestTouchUtils;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.url.GURL;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeoutException;

/** Instrumentation tests for showing the publisher URL for a trusted CDN. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TrustedCdnPublisherUrlTest {
    private final TestRule mModuleOverridesRule =
            new ModuleOverridesRule()
                    .setOverride(
                            BaseCustomTabActivityModule.Factory.class,
                            (BrowserServicesIntentDataProvider intentDataProvider,
                                    CustomTabNightModeStateController nightModeController,
                                    CustomTabIntentHandler.IntentIgnoringCriterion
                                            intentIgnoringCriterion,
                                    TopUiThemeColorProvider topUiThemeColorProvider,
                                    DefaultBrowserProviderImpl customTabDefaultBrowserProvider) ->
                                    new BaseCustomTabActivityModule(
                                            intentDataProvider,
                                            nightModeController,
                                            intentIgnoringCriterion,
                                            topUiThemeColorProvider,
                                            new FakeDefaultBrowserProviderImpl()));

    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();
    public ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_MOBILE_CUSTOM_TABS)
                    .build();

    @Rule
    public RuleChain mRuleChain =
            RuleChain.emptyRuleChain()
                    .around(mRenderTestRule)
                    .around(mCustomTabActivityTestRule)
                    .around(mModuleOverridesRule);

    private static final String PAGE_WITH_TITLE =
            "<!DOCTYPE html><html><head><title>Example title</title></head></html>";

    private TestWebServer mWebServer;

    @Rule public final ScreenShooter mScreenShooter = new ScreenShooter();

    /** Annotation to override the trusted CDN. */
    @Retention(RetentionPolicy.RUNTIME)
    private @interface OverrideTrustedCdn {}

    private static class OverrideTrustedCdnRule extends AnnotationRule {
        public OverrideTrustedCdnRule() {
            super(OverrideTrustedCdn.class);
        }

        /**
         * @return Whether the trusted CDN should be overridden.
         */
        public boolean isEnabled() {
            return !getAnnotations().isEmpty();
        }
    }

    @Rule public OverrideTrustedCdnRule mOverrideTrustedCdn = new OverrideTrustedCdnRule();

    @Before
    public void setUp() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));

        LibraryLoader.getInstance().ensureInitialized();
        mWebServer = TestWebServer.start();
        if (mOverrideTrustedCdn.isEnabled()) {
            CommandLine.getInstance()
                    .appendSwitchWithValue(
                            "trusted-cdn-base-url-for-tests", mWebServer.getBaseUrl());
        }
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        mWebServer.shutdown();
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testHttps() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "https://www.example.com/test",
                "com.example.test",
                "example.com",
                R.drawable.omnibox_https_valid_refresh);
        mScreenShooter.shoot("trustedPublisherUrlHttps");
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testHttp() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "http://example.com/test",
                "com.example.test",
                "example.com",
                R.drawable.omnibox_not_secure_warning);
        mScreenShooter.shoot("trustedPublisherUrlHttp");
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testRtl() throws Exception {
        String publisher =
                "\u200e\u202b\u0645\u0648\u0642\u0639\u002e\u0648\u0632\u0627\u0631"
                    + "\u0629\u002d\u0627\u0644\u0623\u062a\u0635\u0627\u0644\u0627\u062a\u002e\u0645"
                    + "\u0635\u0631\u202c\u200e";
        runTrustedCdnPublisherUrlTest(
                "http://xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/",
                "com.example.test",
                publisher,
                R.drawable.omnibox_not_secure_warning);
        mScreenShooter.shoot("trustedPublisherUrlRtl");
    }

    private int getDefaultSecurityIcon() {
        return R.drawable.omnibox_info;
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testNoHeader() throws Exception {
        runTrustedCdnPublisherUrlTest(null, "com.example.test", null, getDefaultSecurityIcon());
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testMalformedHeader() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "garbage", "com.example.test", null, getDefaultSecurityIcon());
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    // No @OverrideTrustedCdn
    public void testUntrustedCdn() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "https://example.com/test", "com.example.test", null, getDefaultSecurityIcon());
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testPageInfo() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "https://example.com/test",
                "com.example.test",
                "example.com",
                R.drawable.omnibox_https_valid_refresh);
        TestTouchUtils.performClickOnMainSync(
                InstrumentationRegistry.getInstrumentation(),
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.security_button));
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        mScreenShooter.shoot("Page Info");
    }

    // TODO(bauerb): Test an insecure HTTPS connection.

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testNavigateAway() throws Exception {
        runTrustedCdnPublisherUrlTest(
                "https://example.com/test",
                "com.example.test",
                "example.com",
                R.drawable.omnibox_https_valid_refresh);

        String otherTestUrl = mWebServer.setResponse("/other.html", PAGE_WITH_TITLE, null);
        mCustomTabActivityTestRule.loadUrl(otherTestUrl);

        verifyUrl(
                UrlFormatter.formatUrlForSecurityDisplay(
                        otherTestUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS));
        // TODO(bauerb): The security icon is updated via an animation. Find a way to reliably
        // disable animations and verify the icon.
    }

    @Test
    @SmallTest
    @Feature({"UiCatalogue"})
    @OverrideTrustedCdn
    public void testReparent() throws Exception {
        GURL publisherUrl = new GURL("https://example.com/test");
        runTrustedCdnPublisherUrlTest(
                publisherUrl.getSpec(),
                "com.example.test",
                "example.com",
                R.drawable.omnibox_https_valid_refresh);

        final Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(
                                ChromeTabbedActivity.class.getName(), /* result= */ null, false);
        CustomTabActivity customTabActivity = mCustomTabActivityTestRule.getActivity();
        final Tab tab = customTabActivity.getActivityTab();
        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    Assert.assertEquals(publisherUrl, TrustedCdn.getPublisherUrl(tab));
                    customTabActivity
                            .getComponent()
                            .resolveNavigationController()
                            .openCurrentUrlInBrowser();
                    Assert.assertNull(customTabActivity.getActivityTab());
                });

        // Use the extended CriteriaHelper timeout to make sure we get an activity
        final Activity activity =
                monitor.waitForActivityWithTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL);
        Assert.assertNotNull(
                "Monitor did not get an activity before hitting the timeout", activity);
        Assert.assertTrue(
                "Expected activity to be a ChromeActivity, was " + activity.getClass().getName(),
                activity instanceof ChromeActivity);
        final ChromeActivity newActivity = (ChromeActivity) activity;
        CriteriaHelper.pollUiThread(() -> newActivity.getActivityTab() == tab, "Tab did not load");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertNull(TrustedCdn.getPublisherUrl(tab));
                });

        String testUrl = mWebServer.getResponseUrl("/test.html");
        String expectedUrl = UrlFormatter.formatUrlForDisplayOmitScheme(testUrl);

        CriteriaHelper.pollUiThread(
                () -> {
                    UrlBar urlBar = newActivity.findViewById(R.id.url_bar);
                    Criteria.checkThat(urlBar.getText().toString(), Matchers.is(expectedUrl));
                });
    }

    @Test
    @SmallTest
    @OverrideTrustedCdn
    @DisabledTest(message = "Disabled for flakiness! See http://crbug.com/847341")
    public void testOfflinePage() throws TimeoutException {
        String publisherUrl = "https://example.com/test";
        runTrustedCdnPublisherUrlTest(
                publisherUrl, "com.example.test", "example.com",
                R.drawable.omnibox_https_valid_refresh);

        // TODO (https://crbug.com/1063807):  Add incognito mode tests.
        OfflinePageBridge offlinePageBridge =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            Profile profile = ProfileManager.getLastUsedRegularProfile();
                            return OfflinePageBridge.getForProfile(profile);
                        });

        // Wait until the offline page model has been loaded.
        CallbackHelper callback = new CallbackHelper();
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    if (offlinePageBridge.isOfflinePageModelLoaded()) {
                        callback.notifyCalled();
                        return;
                    }
                    offlinePageBridge.addObserver(
                            new OfflinePageBridge.OfflinePageModelObserver() {
                                @Override
                                public void offlinePageModelLoaded() {
                                    callback.notifyCalled();
                                    offlinePageBridge.removeObserver(this);
                                }
                            });
                });
        callback.waitForCallback(0);

        CallbackHelper callback2 = new CallbackHelper();
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    CustomTabActivity customTabActivity = mCustomTabActivityTestRule.getActivity();
                    Tab tab = customTabActivity.getActivityTab();
                    String pageUrl = tab.getUrl().getSpec();
                    offlinePageBridge.savePage(
                            tab.getWebContents(),
                            new ClientId(OfflinePageBridge.DOWNLOAD_NAMESPACE, "1234"),
                            (savePageResult, url, offlineId) -> {
                                Assert.assertEquals(SavePageResult.SUCCESS, savePageResult);
                                Assert.assertEquals(pageUrl, url);
                                // offlineId
                                callback2.notifyCalled();
                            });
                });
        callback2.waitForCallback(0);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    NetworkChangeNotifier.forceConnectivityState(false);
                });

        // Load the URL in the same tab. With no connectivity, loading the offline page should
        // succeed, but not show a publisher URL.
        String testUrl = mWebServer.getResponseUrl("/test.html");
        mCustomTabActivityTestRule.loadUrl(testUrl);
        verifyUrl(
                UrlFormatter.formatUrlForSecurityDisplay(
                        testUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS));
        verifySecurityIcon(R.drawable.ic_offline_pin_24dp);
    }

    private void runTrustedCdnPublisherUrlTest(
            @Nullable String publisherUrl,
            String clientPackage,
            @Nullable String expectedPublisher,
            int expectedSecurityIcon)
            throws TimeoutException {
        final List<Pair<String, String>> headers;
        if (publisherUrl != null) {
            headers = Collections.singletonList(Pair.create("X-AMP-Cache", publisherUrl));
        } else {
            headers = null;
        }
        String testUrl = mWebServer.setResponse("/test.html", PAGE_WITH_TITLE, headers);
        Context targetContext = ApplicationProvider.getApplicationContext();
        Intent intent =
                CustomTabsIntentTestUtils.createMinimalCustomTabIntent(targetContext, testUrl);
        intent.putExtra(
                CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE);
        CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
        CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait();
        connection.newSession(token);
        connection.overridePackageNameForSessionForTesting(token, clientPackage);
        connection.setTrustedPublisherUrlPackageForTest("com.example.test");

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        final String expectedUrl;
        if (expectedPublisher == null) {
            expectedUrl =
                    UrlFormatter.formatUrlForSecurityDisplay(
                            testUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
        } else {
            expectedUrl =
                    String.format(Locale.US, "From %s – delivered by Google", expectedPublisher);
        }
        verifyUrl(expectedUrl);
        verifySecurityIcon(expectedSecurityIcon);
    }

    private void verifyUrl(String expectedUrl) {
        onViewWaiting(allOf(withId(R.id.url_bar), withText(expectedUrl)));
    }

    private void verifySecurityIcon(int expectedSecurityIcon) {
        ImageView securityButton =
                mCustomTabActivityTestRule.getActivity().findViewById(R.id.security_button);

        if (expectedSecurityIcon == 0) {
            Assert.assertEquals(View.INVISIBLE, securityButton.getVisibility());
        } else {
            Assert.assertEquals(View.VISIBLE, securityButton.getVisibility());

            // VectorDrawables don't have a good means for comparison so just verify resource IDs.
            if (securityButton.getDrawable() instanceof VectorDrawable) {
                CustomTabToolbar toolbar =
                        mCustomTabActivityTestRule.getActivity().findViewById(R.id.toolbar);
                CustomTabLocationBar locationBar = (CustomTabLocationBar) toolbar.getLocationBar();
                Resources res = mCustomTabActivityTestRule.getActivity().getResources();
                Assert.assertEquals(
                        res.getResourceName(expectedSecurityIcon),
                        res.getResourceName(locationBar.getSecurityIconResourceForTesting()));
            } else {
                ColorStateList colorStateList =
                        AppCompatResources.getColorStateList(
                                ApplicationProvider.getApplicationContext(),
                                R.color.default_icon_color_light_tint_list);
                ImageView expectedSecurityButton =
                        new ImageView(ApplicationProvider.getApplicationContext());
                expectedSecurityButton.setImageResource(expectedSecurityIcon);
                ImageViewCompat.setImageTintList(expectedSecurityButton, colorStateList);

                BitmapDrawable expectedDrawable =
                    (BitmapDrawable) expectedSecurityButton.getDrawable();
                BitmapDrawable actualDrawable = (BitmapDrawable) securityButton.getDrawable();
                Assert.assertTrue(expectedDrawable.getBitmap().sameAs(actualDrawable.getBitmap()));
            }
        }
    }
}