chromium/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/controller/webapps/WebappDisclosureControllerTest.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.browserservices.ui.controller.webapps;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import static org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel.DISCLOSURE_EVENTS_CALLBACK;
import static org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel.DISCLOSURE_STATE;
import static org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel.DISCLOSURE_STATE_DISMISSED_BY_USER;
import static org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel.DISCLOSURE_STATE_NOT_SHOWN;
import static org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel.DISCLOSURE_STATE_SHOWN;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.android.util.concurrent.RoboExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;

import org.chromium.base.task.PostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappIntentUtils;
import org.chromium.chrome.browser.browserservices.ui.TrustedWebActivityModel;
import org.chromium.chrome.browser.browserservices.ui.controller.CurrentPageVerifier;
import org.chromium.chrome.browser.browserservices.ui.controller.CurrentPageVerifier.VerificationState;
import org.chromium.chrome.browser.browserservices.ui.controller.CurrentPageVerifier.VerificationStatus;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.webapps.WebappDataStorage;
import org.chromium.chrome.browser.webapps.WebappDeferredStartupWithStorageHandler;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.chrome.test.util.browser.webapps.WebApkIntentDataProviderBuilder;
import org.chromium.components.webapk.lib.common.WebApkConstants;

/** Tests for WebappDisclosureController */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
// TODO(crbug.com/40182398): Change to use paused looper. See crbug for details.
@LooperMode(LooperMode.Mode.LEGACY)
public class WebappDisclosureControllerTest {
    private static final String UNBOUND_PACKAGE = "unbound";
    private static final String BOUND_PACKAGE = WebApkConstants.WEBAPK_PACKAGE_PREFIX + ".bound";
    private static final String SCOPE = "https://www.example.com";

    @Mock public CurrentPageVerifier mCurrentPageVerifier;

    @Captor public ArgumentCaptor<Runnable> mVerificationObserverCaptor;

    public TrustedWebActivityModel mModel = new TrustedWebActivityModel();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        // Run AsyncTasks synchronously.
        PostTask.setPrenativeThreadPoolExecutorForTesting(new RoboExecutorService());

        doNothing()
                .when(mCurrentPageVerifier)
                .addVerificationObserver(mVerificationObserverCaptor.capture());
    }

    private WebappDisclosureController buildControllerForWebApk(String webApkPackageName) {
        BrowserServicesIntentDataProvider intentDataProvider =
                new WebApkIntentDataProviderBuilder(webApkPackageName, "https://pwa.rocks/")
                        .build();
        return new WebappDisclosureController(
                intentDataProvider,
                mock(WebappDeferredStartupWithStorageHandler.class),
                mModel,
                mock(ActivityLifecycleDispatcher.class),
                mCurrentPageVerifier);
    }

    private WebappDataStorage registerStorageForWebApk(String packageName) {
        String id = WebappIntentUtils.getIdForWebApkPackage(packageName);
        WebappRegistry.getInstance().register(id, (storage) -> {});
        return WebappRegistry.getInstance().getWebappDataStorage(id);
    }

    public void verifyShownThenDismissedOnNewCreateStorage(String packageName) {
        WebappDisclosureController controller = buildControllerForWebApk(packageName);
        WebappDataStorage storage = registerStorageForWebApk(packageName);
        setVerificationStatus(VerificationStatus.SUCCESS);

        // Simulates the case that shows the disclosure when creating a new storage.
        controller.onDeferredStartupWithStorage(storage, /* didCreateStorage= */ true);
        assertTrue(storage.shouldShowDisclosure());
        assertSnackbarShown();

        // Dismiss the disclosure.
        mModel.get(DISCLOSURE_EVENTS_CALLBACK).onDisclosureAccepted();

        assertSnackbarAccepted();
        assertFalse(storage.shouldShowDisclosure());

        storage.delete();
    }

    public void verifyShownThenDismissedOnRestart(String packageName) {
        WebappDisclosureController controller = buildControllerForWebApk(packageName);
        WebappDataStorage storage = registerStorageForWebApk(packageName);
        setVerificationStatus(VerificationStatus.SUCCESS);

        // Simulates the case that shows the disclosure when finish native initialization.
        storage.setShowDisclosure();
        assertTrue(storage.shouldShowDisclosure());
        controller.onFinishNativeInitialization();
        assertSnackbarShown();

        // Dismiss the disclosure.
        mModel.get(DISCLOSURE_EVENTS_CALLBACK).onDisclosureAccepted();

        assertSnackbarAccepted();
        assertFalse(storage.shouldShowDisclosure());

        storage.delete();
    }

    public void verifyNotShownOnExistingStorageWithoutShouldShowDisclosure(String packageName) {
        WebappDisclosureController controller = buildControllerForWebApk(packageName);
        WebappDataStorage storage = registerStorageForWebApk(packageName);

        // Simulate that starting with existing storage will not cause the disclosure to show.
        assertFalse(storage.shouldShowDisclosure());
        controller.onDeferredStartupWithStorage(storage, /* didCreateStorage= */ false);
        assertSnackbarNotShown();

        storage.delete();
    }

    public void verifyNeverShown(String packageName) {
        WebappDisclosureController controller = buildControllerForWebApk(packageName);
        WebappDataStorage storage = registerStorageForWebApk(packageName);

        // Try to show the disclosure the first time.
        controller.onDeferredStartupWithStorage(storage, /* didCreateStorage= */ true);
        assertSnackbarNotShown();

        // Try to the disclosure again this time emulating a restart.
        controller.onFinishNativeInitialization();
        assertSnackbarNotShown();

        storage.delete();
    }

    private void assertSnackbarShown() {
        assertEquals(DISCLOSURE_STATE_SHOWN, mModel.get(DISCLOSURE_STATE));
    }

    private void assertSnackbarAccepted() {
        assertEquals(DISCLOSURE_STATE_DISMISSED_BY_USER, mModel.get(DISCLOSURE_STATE));
    }

    private void assertSnackbarNotShown() {
        assertEquals(DISCLOSURE_STATE_NOT_SHOWN, mModel.get(DISCLOSURE_STATE));
    }

    private void setVerificationStatus(@VerificationStatus int status) {
        VerificationState state = new VerificationState(SCOPE, SCOPE, status);
        doReturn(state).when(mCurrentPageVerifier).getState();

        for (Runnable observer : mVerificationObserverCaptor.getAllValues()) {
            observer.run();
        }
    }

    @Test
    @Feature({"Webapps"})
    public void testUnboundWebApkShowDisclosure() {
        verifyShownThenDismissedOnNewCreateStorage(UNBOUND_PACKAGE);
    }

    @Test
    @Feature({"Webapps"})
    public void testUnboundWebApkShowDisclosure2() {
        verifyShownThenDismissedOnRestart(UNBOUND_PACKAGE);
    }

    @Test
    @Feature({"Webapps"})
    public void testUnboundWebApkNoDisclosureOnExistingStorage() {
        verifyNotShownOnExistingStorageWithoutShouldShowDisclosure(UNBOUND_PACKAGE);
    }

    @Test
    @Feature({"Webapps"})
    public void testBoundWebApkNoDisclosure() {
        verifyNeverShown(BOUND_PACKAGE);
    }

    @Test
    @Feature({"Webapps"})
    public void testNotShowDisclosureWhenNotVerifiedOrigin() {
        WebappDisclosureController controller = buildControllerForWebApk(UNBOUND_PACKAGE);
        WebappDataStorage storage = registerStorageForWebApk(UNBOUND_PACKAGE);

        setVerificationStatus(VerificationStatus.FAILURE);
        controller.onDeferredStartupWithStorage(storage, /* didCreateStorage= */ true);
        assertTrue(storage.shouldShowDisclosure());

        assertSnackbarNotShown();
    }

    @Test
    @Feature({"Webapps"})
    public void testDismissDisclosureWhenLeavingVerifiedOrigin() {
        WebappDisclosureController controller = buildControllerForWebApk(UNBOUND_PACKAGE);
        WebappDataStorage storage = registerStorageForWebApk(UNBOUND_PACKAGE);
        storage.setShowDisclosure();
        controller.onFinishNativeInitialization();

        setVerificationStatus(VerificationStatus.SUCCESS);
        assertSnackbarShown();

        setVerificationStatus(VerificationStatus.FAILURE);
        assertSnackbarNotShown();
    }

    @Test
    @Feature({"Webapps"})
    public void testShowsAgainWhenReenteringTrustedOrigin() {
        WebappDisclosureController controller = buildControllerForWebApk(UNBOUND_PACKAGE);
        WebappDataStorage storage = registerStorageForWebApk(UNBOUND_PACKAGE);
        storage.setShowDisclosure();
        controller.onFinishNativeInitialization();

        setVerificationStatus(VerificationStatus.SUCCESS);
        setVerificationStatus(VerificationStatus.FAILURE);
        setVerificationStatus(VerificationStatus.SUCCESS);
        assertSnackbarShown();
    }
}