| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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 org.chromium.chrome.browser.init.ChromeBrowserInitializer.PRIVATE_DATA_DIRECTORY_SUFFIX; |
| |
| import android.app.Activity; |
| import android.app.Instrumentation; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.support.annotation.Nullable; |
| import android.support.customtabs.CustomTabsIntent; |
| import android.support.customtabs.CustomTabsSessionToken; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.SmallTest; |
| import android.util.Pair; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.CommandLine; |
| import org.chromium.base.PathUtils; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.library_loader.LibraryLoader; |
| import org.chromium.base.library_loader.LibraryProcessType; |
| 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.DisabledTest; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.ChromeSwitches; |
| import org.chromium.chrome.browser.ChromeTabbedActivity; |
| import org.chromium.chrome.browser.firstrun.FirstRunStatus; |
| 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.tab.Tab; |
| import org.chromium.chrome.browser.test.ScreenShooter; |
| import org.chromium.chrome.test.ChromeJUnit4ClassRunner; |
| import org.chromium.chrome.test.util.browser.Features; |
| import org.chromium.components.offlinepages.SavePageResult; |
| import org.chromium.components.url_formatter.UrlFormatter; |
| import org.chromium.content_public.browser.test.util.Criteria; |
| import org.chromium.content_public.browser.test.util.CriteriaHelper; |
| import org.chromium.content_public.browser.test.util.TestTouchUtils; |
| import org.chromium.net.NetworkChangeNotifier; |
| import org.chromium.net.test.util.TestWebServer; |
| import org.chromium.ui.base.DeviceFormFactor; |
| |
| 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 { |
| @Rule |
| public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule(); |
| |
| 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)); |
| |
| PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); |
| LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER); |
| mWebServer = TestWebServer.start(); |
| if (mOverrideTrustedCdn.isEnabled()) { |
| CommandLine.getInstance().appendSwitchWithValue( |
| "trusted-cdn-base-url-for-tests", mWebServer.getBaseUrl()); |
| } |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false)); |
| |
| mWebServer.shutdown(); |
| } |
| |
| @Test |
| //@SmallTest |
| //@Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/851950 |
| public void testHttps() throws Exception { |
| runTrustedCdnPublisherUrlTest("https://www.example.com/test", "com.example.test", |
| "example.com", org.chromium.chrome.R.drawable.omnibox_https_valid); |
| mScreenShooter.shoot("trustedPublisherUrlHttps"); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| public void testHttp() throws Exception { |
| runTrustedCdnPublisherUrlTest("http://example.com/test", "com.example.test", "example.com", |
| org.chromium.chrome.R.drawable.omnibox_info); |
| mScreenShooter.shoot("trustedPublisherUrlHttp"); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| 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, org.chromium.chrome.R.drawable.omnibox_info); |
| mScreenShooter.shoot("trustedPublisherUrlRtl"); |
| } |
| |
| private int getDefaultSecurityIcon() { |
| // On tablets an info icon is shown for ConnectionSecurityLevel.NONE pages, |
| // on smaller form factors nothing. |
| if (DeviceFormFactor.isNonMultiDisplayContextOnTablet( |
| InstrumentationRegistry.getTargetContext())) { |
| return org.chromium.chrome.R.drawable.omnibox_info; |
| } |
| |
| return 0; |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| public void testUntrustedClient() throws Exception { |
| runTrustedCdnPublisherUrlTest( |
| "https://example.com/test", "com.someoneelse.bla", null, getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| public void testNoHeader() throws Exception { |
| runTrustedCdnPublisherUrlTest(null, "com.example.test", null, getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| public void testMalformedHeader() throws Exception { |
| runTrustedCdnPublisherUrlTest( |
| "garbage", "com.example.test", null, getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.DisableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| public void testDisabled() throws Exception { |
| runTrustedCdnPublisherUrlTest( |
| "https://example.com/test", "com.example.test", null, getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| // No @OverrideTrustedCdn |
| public void testUntrustedCdn() throws Exception { |
| runTrustedCdnPublisherUrlTest( |
| "https://example.com/test", "com.example.test", null, getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"UiCatalogue"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| public void testPageInfo() throws Exception { |
| runTrustedCdnPublisherUrlTest("https://example.com/test", "com.example.test", "example.com", |
| R.drawable.omnibox_https_valid); |
| 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"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| public void testNavigateAway() throws Exception { |
| runTrustedCdnPublisherUrlTest("https://example.com/test", "com.example.test", "example.com", |
| R.drawable.omnibox_https_valid); |
| |
| String otherTestUrl = mWebServer.setResponse("/other.html", PAGE_WITH_TITLE, null); |
| mCustomTabActivityTestRule.loadUrl(otherTestUrl); |
| |
| verifyUrl(UrlFormatter.formatUrlForSecurityDisplayOmitScheme(otherTestUrl)); |
| // 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"}) |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| public void testReparent() throws Exception { |
| String publisherUrl = "https://example.com/test"; |
| runTrustedCdnPublisherUrlTest( |
| publisherUrl, "com.example.test", "example.com", R.drawable.omnibox_https_valid); |
| |
| final Instrumentation.ActivityMonitor monitor = |
| InstrumentationRegistry.getInstrumentation().addMonitor( |
| ChromeTabbedActivity.class.getName(), /* result = */ null, false); |
| CustomTabActivity customTabActivity = mCustomTabActivityTestRule.getActivity(); |
| final Tab tab = customTabActivity.getActivityTab(); |
| ThreadUtils.postOnUiThread(() -> { |
| Assert.assertEquals(publisherUrl, tab.getTrustedCdnPublisherUrl()); |
| customTabActivity.openCurrentUrlInBrowser(true); |
| 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(tab.getTrustedCdnPublisherUrl()); }); |
| |
| String testUrl = mWebServer.getResponseUrl("/test.html"); |
| String expectedUrl = UrlFormatter.formatUrlForDisplayOmitScheme(testUrl); |
| |
| CriteriaHelper.pollUiThread(Criteria.equals(expectedUrl, () -> { |
| UrlBar urlBar = newActivity.findViewById(R.id.url_bar); |
| return urlBar.getText().toString(); |
| })); |
| |
| verifySecurityIcon(getDefaultSecurityIcon()); |
| } |
| |
| @Test |
| @SmallTest |
| @Features.EnableFeatures(ChromeFeatureList.SHOW_TRUSTED_PUBLISHER_URL) |
| @OverrideTrustedCdn |
| @DisabledTest // Disabled for flakiness! See http://crbug.com/847341 |
| public void testOfflinePage() throws TimeoutException, InterruptedException { |
| String publisherUrl = "https://example.com/test"; |
| runTrustedCdnPublisherUrlTest( |
| publisherUrl, "com.example.test", "example.com", R.drawable.omnibox_https_valid); |
| |
| OfflinePageBridge offlinePageBridge = ThreadUtils.runOnUiThreadBlockingNoException(() -> { |
| Profile profile = Profile.getLastUsedProfile(); |
| return OfflinePageBridge.getForProfile(profile); |
| }); |
| |
| // Wait until the offline page model has been loaded. |
| CallbackHelper callback = new CallbackHelper(); |
| ThreadUtils.runOnUiThread(() -> { |
| 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(); |
| ThreadUtils.runOnUiThread(() -> { |
| CustomTabActivity customTabActivity = mCustomTabActivityTestRule.getActivity(); |
| Tab tab = customTabActivity.getActivityTab(); |
| String pageUrl = tab.getUrl(); |
| 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.formatUrlForSecurityDisplayOmitScheme(testUrl)); |
| verifySecurityIcon(R.drawable.offline_pin_round); |
| } |
| |
| private void runTrustedCdnPublisherUrlTest(@Nullable String publisherUrl, String clientPackage, |
| @Nullable String expectedPublisher, int expectedSecurityIcon) |
| throws InterruptedException, 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 = InstrumentationRegistry.getTargetContext(); |
| Intent intent = CustomTabsTestUtils.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.formatUrlForSecurityDisplayOmitScheme(testUrl); |
| } else { |
| expectedUrl = |
| String.format(Locale.US, "From %s – delivered by Google", expectedPublisher); |
| } |
| verifyUrl(expectedUrl); |
| verifySecurityIcon(expectedSecurityIcon); |
| } |
| |
| private void verifyUrl(String expectedUrl) { |
| UrlBar urlBar = mCustomTabActivityTestRule.getActivity().findViewById(R.id.url_bar); |
| Assert.assertEquals(expectedUrl, urlBar.getText().toString()); |
| } |
| |
| 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()); |
| Bitmap expectedIcon = BitmapFactory.decodeResource( |
| InstrumentationRegistry.getTargetContext().getResources(), |
| expectedSecurityIcon); |
| Assert.assertTrue(expectedIcon.sameAs( |
| ((BitmapDrawable) securityButton.getDrawable()).getBitmap())); |
| } |
| } |
| } |