chromium/chrome/browser/supervised_user/android/javatests/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalNativesTest.java

// Copyright 2023 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.supervised_user;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import androidx.test.filters.MediumTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.superviseduser.FilteringBehavior;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.browser.signin.SigninTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport;
import org.chromium.content_public.browser.WebContents;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;

import java.util.concurrent.TimeoutException;

/**
 * Tests the local website approval flow. This test suite invokes the actual native methods and
 * allows us to execute more of the code, compared to {@link
 * org.chromium.chrome.browser.supervised_user.WebsiteParentApprovalTest} whick mocks the natives.
 * This allows us to test all histograms recorded by natives.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(
        reason =
                "Running tests in parallel can interfere with each tests setup."
                        + "The code under tests involes setting static methods and features, "
                        + "which must remain unchanged for the duration of the test.")
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class WebsiteParentApprovalNativesTest {
    // TODO(b/243916194): Expand the test coverage beyond the completion callback, up to the page
    // refresh.

    public ChromeTabbedActivityTestRule mTabbedActivityTestRule =
            new ChromeTabbedActivityTestRule();
    public SigninTestRule mSigninTestRule = new SigninTestRule();

    // Destroy TabbedActivityTestRule before SigninTestRule to remove observers of
    // FakeAccountManagerFacade.
    @Rule
    public final RuleChain mRuleChain =
            RuleChain.outerRule(mSigninTestRule).around(mTabbedActivityTestRule);

    @Rule
    public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    private static final String TEST_PAGE = "/chrome/test/data/android/about.html";

    private EmbeddedTestServer mTestServer;
    private String mBlockedUrl;
    private WebContents mWebContents;
    private BottomSheetController mBottomSheetController;
    private BottomSheetTestSupport mBottomSheetTestSupport;

    @Mock private ParentAuthDelegate mParentAuthDelegateMock;

    @Before
    public void setUp() throws TimeoutException {
        mTestServer = mTabbedActivityTestRule.getEmbeddedTestServerRule().getServer();
        mBlockedUrl = mTestServer.getURL(TEST_PAGE);
        mTabbedActivityTestRule.startMainActivityOnBlankPage();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChromeTabbedActivity activity = mTabbedActivityTestRule.getActivity();
                    mBottomSheetController =
                            activity.getRootUiCoordinatorForTesting().getBottomSheetController();
                    mBottomSheetTestSupport = new BottomSheetTestSupport(mBottomSheetController);
                });

        mSigninTestRule.addChildTestAccountThenWaitForSignin();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    SupervisedUserSettingsTestBridge.setFilteringBehavior(
                            mTabbedActivityTestRule.getProfile(/* incognito= */ false),
                            FilteringBehavior.BLOCK);
                });
        mWebContents = mTabbedActivityTestRule.getWebContents();

        // TODO(b/243916194): Once we start consuming mParentAuthDelegateMock
        // .isLocalAuthSupported we should add a mocked behaviour in this test.
        ParentAuthDelegateProvider.setInstanceForTests(mParentAuthDelegateMock);
    }

    private void mockParentAuthDelegateRequestLocalAuthResponse(boolean result) {
        doAnswer(
                        invocation -> {
                            Callback<Boolean> onCompletionCallback = invocation.getArgument(2);
                            onCompletionCallback.onResult(result);
                            return null;
                        })
                .when(mParentAuthDelegateMock)
                .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class));
    }

    @Test
    @MediumTest
    public void parentApprovesLocally() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectAnyRecord(
                                "FamilyLinkUser.LocalWebApprovalCompleteRequestTotalDuration")
                        .expectIntRecord("FamilyLinkUser.LocalWebApprovalResult", /* Approved= */ 0)
                        .build();

        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);
        WebsiteParentApprovalTestUtils.clickApprove(mBottomSheetTestSupport);

        // Delay to ensure the asynchronous code that records the histograms is executed.
        verify(mParentAuthDelegateMock, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class));

        histograms.pollInstrumentationThreadUntilSatisfied();
    }

    @Test
    @MediumTest
    public void parentRejectsLocally() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectAnyRecord(
                                "FamilyLinkUser.LocalWebApprovalCompleteRequestTotalDuration")
                        .expectIntRecord("FamilyLinkUser.LocalWebApprovalResult", /* Declined= */ 1)
                        .build();

        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);
        WebsiteParentApprovalTestUtils.clickDoNotApprove(mBottomSheetTestSupport);

        // Delay to ensure the asynchronous code that records the histograms is executed.
        verify(mParentAuthDelegateMock, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class));

        histograms.pollInstrumentationThreadUntilSatisfied();
    }

    @Test
    @MediumTest
    public void parentAuthorizationFailure() {
        mockParentAuthDelegateRequestLocalAuthResponse(false);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);
        var histograms =
                HistogramWatcher.newSingleRecordWatcher(
                        "FamilyLinkUser.LocalWebApprovalResult", /* Cancelled= */ 2);

        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);

        // Delay to ensure the asynchronous code that records the histograms is executed.
        verify(mParentAuthDelegateMock, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class));

        histograms.pollInstrumentationThreadUntilSatisfied();
    }

    @Test
    @MediumTest
    public void cancelApprovalRequestIfOneAlreadyInProgress() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);
        var histograms =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                "FamilyLinkUser.LocalWebApprovalResult",
                                /* Approved= */ 0,
                                /* Cancelled= */ 2)
                        .build();

        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);
        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);

        WebsiteParentApprovalTestUtils.clickApprove(mBottomSheetTestSupport);

        // Delay to ensure the asynchronous code that records the histograms is executed.
        verify(mParentAuthDelegateMock, timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class));

        histograms.pollInstrumentationThreadUntilSatisfied();
    }
}