chromium/chrome/android/javatests/src/org/chromium/chrome/browser/subresource_filter/SubresourceFilterTest.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.subresource_filter;

import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;

import androidx.test.espresso.Espresso;
import androidx.test.filters.LargeTest;

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.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.MockSafeBrowsingApiHandler;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.infobar.InfoBarContainer;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.components.browser_ui.modaldialog.TabModalPresenter;
import org.chromium.components.infobars.InfoBar;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageStateHandler;
import org.chromium.components.messages.MessagesTestHelper;
import org.chromium.components.safe_browsing.SafeBrowsingApiBridge;
import org.chromium.components.subresource_filter.AdsBlockedInfoBar;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.EmbeddedTestServerRule;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.UiRestriction;

import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * End to end tests of SubresourceFilter ad filtering on Android.
 *
 * <p>Since these tests take a while to set up (averaging 12 seconds between activity startup and
 * ruleset publishing), prefer to limit the number of test cases where possible.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public final class SubresourceFilterTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Rule public EmbeddedTestServerRule mTestServerRule = new EmbeddedTestServerRule();

    private EmbeddedTestServer mTestServer;

    private static final String PAGE_WITH_JPG =
            "/chrome/test/data/android/subresource_filter/page-with-img.html";
    private static final String LEARN_MORE_PAGE =
            "https://support.google.com/chrome/?p=blocked_ads";
    private static final String METADATA_FOR_ENFORCEMENT =
            "{\"matches\":[{\"threat_type\":\"13\",\"sf_bas\":\"\"}]}";
    private static final String METADATA_FOR_WARNING =
            "{\"matches\":[{\"threat_type\":\"13\",\"sf_bas\":\"warn\"}]}";

    private void createAndPublishRulesetDisallowingSuffix(String suffix) {
        TestRulesetPublisher publisher = new TestRulesetPublisher();
        ThreadUtils.runOnUiThreadBlocking(
                () -> publisher.createAndPublishRulesetDisallowingSuffixForTesting(suffix));

        // This takes an average of 6 seconds but can range anywhere from 2-10 seconds on occasion.
        // Since we are testing startup events, ensuring that they fire, this delay is unavoidable.
        // Increase the timeout to 15 seconds to remove flakiness.
        CriteriaHelper.pollUiThread(
                publisher::isPublished, 15000L, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    @Before
    public void setUp() throws Exception {
        mTestServer = mTestServerRule.getServer();
        SafeBrowsingApiBridge.setSafeBrowsingApiHandler(new MockSafeBrowsingApiHandler());
        mActivityTestRule.startMainActivityOnBlankPage();

        // Disallow all jpgs.
        createAndPublishRulesetDisallowingSuffix(".jpg");
    }

    @After
    public void tearDown() {
        MockSafeBrowsingApiHandler.clearMockResponses();
        SafeBrowsingApiBridge.clearHandlerForTesting();
    }

    @Test
    @LargeTest
    public void resourceNotFiltered() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        mActivityTestRule.loadUrl(url);

        String loaded = mActivityTestRule.runJavaScriptCodeInCurrentTab("imgLoaded");
        Assert.assertEquals("true", loaded);

        // Check that the infobar is not showing.
        List<InfoBar> infoBars = mActivityTestRule.getInfoBars();
        CriteriaHelper.pollUiThread(() -> infoBars.isEmpty());
    }

    @Test
    @LargeTest
    @DisableFeatures(ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED)
    public void resourceFilteredClose_InfobarUI() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertFalse(loadPageWithBlockableContentAndTestIfBlocked(url, false));

        // Check that the infobar is showing.
        List<InfoBar> infoBars = mActivityTestRule.getInfoBars();
        CriteriaHelper.pollUiThread(() -> infoBars.size() == 1);
        AdsBlockedInfoBar infobar = (AdsBlockedInfoBar) infoBars.get(0);

        // Click the link once to expand it.
        ThreadUtils.runOnUiThreadBlocking(infobar::onLinkClicked);

        // Check the checkbox and press the button to reload.
        ThreadUtils.runOnUiThreadBlocking(() -> infobar.onCheckedChanged(null, true));

        // Think better of it and just close the infobar.
        ThreadUtils.runOnUiThreadBlocking(infobar::onCloseButtonClicked);
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        CriteriaHelper.pollUiThread(() -> !InfoBarContainer.get(tab).hasInfoBars());
    }

    @Test
    @LargeTest
    @DisableFeatures(ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED)
    public void resourceFilteredClickLearnMore_InfobarUI() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertFalse(loadPageWithBlockableContentAndTestIfBlocked(url, false));

        Tab originalTab = mActivityTestRule.getActivity().getActivityTab();
        CallbackHelper tabCreatedCallback = new CallbackHelper();
        TabModel tabModel = mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        tabModel.addObserver(
                                new TabModelObserver() {
                                    @Override
                                    public void didAddTab(
                                            Tab tab,
                                            @TabLaunchType int type,
                                            @TabCreationState int creationState,
                                            boolean markedForSelection) {
                                        if (tab.getUrl().getSpec().equals(LEARN_MORE_PAGE)) {
                                            tabCreatedCallback.notifyCalled();
                                        }
                                    }
                                }));

        // Check that the infobar is showing.
        List<InfoBar> infoBars = mActivityTestRule.getInfoBars();
        CriteriaHelper.pollUiThread(() -> infoBars.size() == 1);
        AdsBlockedInfoBar infobar = (AdsBlockedInfoBar) infoBars.get(0);

        // Click the link once to expand it.
        ThreadUtils.runOnUiThreadBlocking(infobar::onLinkClicked);

        // Click again to navigate, which should spawn a new tab.
        ThreadUtils.runOnUiThreadBlocking(infobar::onLinkClicked);

        // Wait for the tab to be added with the correct URL. Note, do not wait for this URL to be
        // loaded since it is not controlled by the test instrumentation. Just waiting for the
        // navigation to start should be OK though.
        tabCreatedCallback.waitForCallback("Never received tab created event", 0);

        // The infobar should not be removed on the original tab.
        CriteriaHelper.pollUiThread(() -> InfoBarContainer.get(originalTab).hasInfoBars());
    }

    @Test
    @LargeTest
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    @EnableFeatures({ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED})
    public void resourceFilteredClickLearnMore_MessagesUI_ReshowDialogOnPhoneOnBackPress()
            throws Exception {
        testResourceFilteredClickLearnMore_MessagesUIFlow();
    }

    @Test
    @LargeTest
    @Restriction(UiRestriction.RESTRICTION_TYPE_TABLET)
    @EnableFeatures({ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED})
    public void resourceFilteredClickLearnMore_MessagesUI_ReshowDialogOnTabletOnBackPress()
            throws Exception {
        testResourceFilteredClickLearnMore_MessagesUIFlow();
    }

    @Test
    @LargeTest
    @DisableFeatures(ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED)
    public void resourceFilteredReload_InfobarUI() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertFalse(loadPageWithBlockableContentAndTestIfBlocked(url, false));

        // Check that the infobar is showing.
        List<InfoBar> infoBars = mActivityTestRule.getInfoBars();
        CriteriaHelper.pollUiThread(() -> infoBars.size() == 1);
        AdsBlockedInfoBar infobar = (AdsBlockedInfoBar) infoBars.get(0);

        // Click the link once to expand it.
        ThreadUtils.runOnUiThreadBlocking(infobar::onLinkClicked);

        // Check the checkbox and press the button to reload.
        ThreadUtils.runOnUiThreadBlocking(() -> infobar.onCheckedChanged(null, true));
        ThreadUtils.runOnUiThreadBlocking(() -> infobar.onButtonClicked(true));

        Assert.assertTrue(verifyPageReloadedWithOriginalContent(url));
    }

    @Test
    @LargeTest
    @EnableFeatures({ChromeFeatureList.MESSAGES_FOR_ANDROID_ADS_BLOCKED})
    public void resourceFilteredReload_MessagesUI() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertFalse(loadPageWithBlockableContentAndTestIfBlocked(url, false));

        // Check that the Ads Blocked message is showing and get the active message.
        PropertyModel message = verifyAndGetAdsBlockedMessage();

        // Trigger the Ads Blocked dialog and simulate the dialog positive button click.
        PropertyModel adsBlockedDialog = createAdsBlockedDialog(message);
        ModalDialogProperties.Controller dialogController =
                adsBlockedDialog.get(ModalDialogProperties.CONTROLLER);
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        dialogController.onClick(
                                adsBlockedDialog, ModalDialogProperties.ButtonType.POSITIVE));

        Assert.assertTrue(verifyPageReloadedWithOriginalContent(url));
    }

    @Test
    @LargeTest
    public void resourceNotFilteredWithWarning() throws Exception {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertTrue(loadPageWithBlockableContentAndTestIfBlocked(url, true));

        // Check that the infobar is not showing.
        List<InfoBar> infoBars = mActivityTestRule.getInfoBars();
        CriteriaHelper.pollUiThread(() -> infoBars.isEmpty());
    }

    private void testResourceFilteredClickLearnMore_MessagesUIFlow()
            throws TimeoutException, ExecutionException, InterruptedException {
        String url = mTestServer.getURL(PAGE_WITH_JPG);
        Assert.assertFalse(loadPageWithBlockableContentAndTestIfBlocked(url, false));

        CallbackHelper tabCreatedCallback = new CallbackHelper();
        TabModel tabModel = mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        tabModel.addObserver(
                                new TabModelObserver() {
                                    @Override
                                    public void didAddTab(
                                            Tab tab,
                                            @TabLaunchType int type,
                                            @TabCreationState int creationState,
                                            boolean markedForSelection) {
                                        if (tab.getUrl().getSpec().equals(LEARN_MORE_PAGE)) {
                                            tabCreatedCallback.notifyCalled();
                                        }
                                    }
                                }));

        // Check that the Ads Blocked message is showing and get the active message.
        PropertyModel message = verifyAndGetAdsBlockedMessage();

        int currentTabCreatedCallbackCount = tabCreatedCallback.getCallCount();

        // Trigger the Ads Blocked dialog and simulate the "Learn more" link click.
        createAdsBlockedDialog(message);
        View dialogView =
                ((TabModalPresenter)
                                mActivityTestRule
                                        .getActivity()
                                        .getModalDialogManager()
                                        .getCurrentPresenterForTest())
                        .getDialogContainerForTest();
        TextView messageView = dialogView.findViewById(R.id.message_paragraph_1);
        Spanned spannedMessage = (Spanned) messageView.getText();
        ClickableSpan[] spans =
                spannedMessage.getSpans(0, spannedMessage.length(), ClickableSpan.class);
        Assert.assertEquals(
                "Ads Blocked dialog message text must have only 1 ClickableSpan.", 1, spans.length);
        ThreadUtils.runOnUiThreadBlocking(() -> spans[0].onClick(messageView));

        // Wait for the tab to be added with the correct URL. Note, do not wait for this URL to be
        // loaded since it is not controlled by the test instrumentation. Just waiting for the
        // navigation to start should be OK though.
        tabCreatedCallback.waitForCallback(
                "Never received tab created event", currentTabCreatedCallbackCount);

        // Press the back button to go to the original tab where the dialog was shown.
        Espresso.pressBack();

        CriteriaHelper.pollUiThread(
                () -> {
                    // Verify that the dialog is re-shown on the original tab.
                    return mActivityTestRule
                                    .getActivity()
                                    .getModalDialogManager()
                                    .getCurrentDialogForTest()
                            != null;
                },
                "The dialog should be re-shown on navigation to the original tab.");
    }

    private boolean loadPageWithBlockableContentAndTestIfBlocked(String url, boolean isForWarning)
            throws TimeoutException {
        int[] threatAttribute =
                isForWarning
                        ? new int[] {MockSafeBrowsingApiHandler.THREAT_ATTRIBUTE_CANARY_CODE}
                        : new int[0];
        MockSafeBrowsingApiHandler.addMockResponse(
                url, MockSafeBrowsingApiHandler.BETTER_ADS_VIOLATION_CODE, threatAttribute);
        mActivityTestRule.loadUrl(url);
        return Boolean.parseBoolean(mActivityTestRule.runJavaScriptCodeInCurrentTab("imgLoaded"));
    }

    private PropertyModel verifyAndGetAdsBlockedMessage() throws ExecutionException {
        MessageDispatcher messageDispatcher =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                MessageDispatcherProvider.from(
                                        mActivityTestRule.getActivity().getWindowAndroid()));
        List<MessageStateHandler> messages =
                MessagesTestHelper.getEnqueuedMessages(
                        messageDispatcher, MessageIdentifier.ADS_BLOCKED);
        return MessagesTestHelper.getCurrentMessage(messages.get(0));
    }

    private PropertyModel createAdsBlockedDialog(PropertyModel message) {
        // Simulate the message secondary button click.
        Runnable secondaryActionCallback = message.get(MessageBannerProperties.ON_SECONDARY_ACTION);
        ThreadUtils.runOnUiThreadBlocking(secondaryActionCallback);

        // Retrieve the Ads Blocked dialog.
        ModalDialogManager modalDialogManager =
                mActivityTestRule.getActivity().getModalDialogManager();
        return modalDialogManager.getCurrentDialogForTest();
    }

    private boolean verifyPageReloadedWithOriginalContent(String url) throws TimeoutException {
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        ChromeTabUtils.waitForTabPageLoaded(tab, url);

        CriteriaHelper.pollUiThread(() -> !InfoBarContainer.get(tab).hasInfoBars());

        // Reloading should allowlist the site, so resources should no longer be filtered.
        return Boolean.parseBoolean(mActivityTestRule.runJavaScriptCodeInCurrentTab("imgLoaded"));
    }
}