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

// Copyright 2022 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 androidx.test.espresso.matcher.ViewMatchers.withId;

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.base.test.util.JniMocker;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.supervised_user.android.AndroidLocalWebApprovalFlowOutcome;
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.ui.test.util.ViewUtils;
import org.chromium.url.GURL;

import java.util.concurrent.TimeoutException;

/**
 * Tests the local website approval flow. This test suire mocks the natives to allows us to
 * explicitly check the actual completion callback is called. It also confirms the histograms
 * recorded by on the Android specific (Java) part. A verification flow with the actual native
 * methods is captured in {@link
 * org.chromium.chrome.browser.supervised_user.WebsiteParentApprovalNativesTest}.
 */
@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 WebsiteParentApprovalTest {
    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 JniMocker mocker = new JniMocker();

    @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 WebsiteParentApproval.Natives mWebsiteParentApprovalNativesMock;
    @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();

        mocker.mock(WebsiteParentApprovalJni.TEST_HOOKS, mWebsiteParentApprovalNativesMock);

        // @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));
    }

    private void checkParentApprovalScreenClosedAfterClick() {
        // Ensure all animations have ended. Otherwise the following check may fail.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mBottomSheetTestSupport.endAllAnimations();
                });
        ViewUtils.waitForViewCheckingState(
                withId(R.id.local_parent_approval_layout),
                ViewUtils.VIEW_INVISIBLE | ViewUtils.VIEW_GONE | ViewUtils.VIEW_NULL);
    }

    @Test
    @MediumTest
    public void parentApprovesScreenVisibilityAfterApproval() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);

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

        checkParentApprovalScreenClosedAfterClick();
    }

    @Test
    @MediumTest
    public void parentApprovesScreenVisibilityAfterRejection() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);

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

        checkParentApprovalScreenClosedAfterClick();
    }

    @Test
    @MediumTest
    public void parentApprovesLocally() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);

        // Verify only histograms recorded in Java.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "FamilyLinkUser.LocalWebApprovalOutcome", /* APPROVED_BY_PARENT= */ 0);

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

        verify(
                        mWebsiteParentApprovalNativesMock,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .onCompletion(AndroidLocalWebApprovalFlowOutcome.APPROVED);
        histogram.assertExpected();
    }

    @Test
    @MediumTest
    public void parentRejectsLocally() {
        mockParentAuthDelegateRequestLocalAuthResponse(true);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);

        // Verify only histograms recorded in Java.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "FamilyLinkUser.LocalWebApprovalOutcome", /* DENIED_BY_PARENT= */ 1);

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

        verify(
                        mWebsiteParentApprovalNativesMock,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .onCompletion(AndroidLocalWebApprovalFlowOutcome.REJECTED);
        histogram.assertExpected();
    }

    @Test
    @MediumTest
    public void parentAuthorizationFailure() {
        mockParentAuthDelegateRequestLocalAuthResponse(false);
        mTabbedActivityTestRule.loadUrl(mBlockedUrl);

        WebsiteParentApprovalTestUtils.clickAskInPerson(mWebContents);

        verify(
                        mWebsiteParentApprovalNativesMock,
                        timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1))
                .onCompletion(AndroidLocalWebApprovalFlowOutcome.INCOMPLETE);
    }
}