chromium/components/payments/content/android/junit/src/org/chromium/components/payments/secure_payment_confirmation/SecurePaymentConfirmationNoMatchingCredTest.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.components.payments.secure_payment_confirmation;

import android.content.Context;
import android.view.View;

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.mockito.Answers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.JniMocker;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.payments.InputProtector;
import org.chromium.components.payments.test_support.FakeClock;
import org.chromium.components.url_formatter.SchemeDisplay;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.components.url_formatter.UrlFormatterJni;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;

import java.lang.ref.WeakReference;

/** A test for SecurePaymentConfirmationNoMatchingCred. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {
            SecurePaymentConfirmationNoMatchingCredTest.ShadowBottomSheetControllerProvider.class
        })
public class SecurePaymentConfirmationNoMatchingCredTest {
    private static final long IGNORED_INPUT_DELAY =
            InputProtector.POTENTIALLY_UNINTENDED_INPUT_THRESHOLD - 100;
    private static final long SAFE_INPUT_DELAY =
            InputProtector.POTENTIALLY_UNINTENDED_INPUT_THRESHOLD;

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.WARN);
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private WebContents mWebContents;

    private boolean mUserResponded;
    private boolean mUserOptedOut;
    private Runnable mResponseCallback;
    private Runnable mOptOutCallback;
    private FakeClock mClock;

    private SecurePaymentConfirmationNoMatchingCredController mNoMatchingCredController;

    /** The shadow of BottomSheetControllerProvider. Not to use outside the test. */
    @Implements(BottomSheetControllerProvider.class)
    /* package */ static class ShadowBottomSheetControllerProvider {
        private static BottomSheetController sBottomSheetController;

        @Implementation
        public static BottomSheetController from(WindowAndroid windowAndroid) {
            return sBottomSheetController;
        }

        private static void setBottomSheetController(BottomSheetController controller) {
            sBottomSheetController = controller;
        }
    }

    @Before
    public void setUp() {
        WindowAndroid windowAndroid = Mockito.mock(WindowAndroid.class);
        setWindowAndroid(windowAndroid, mWebContents);
        Mockito.doReturn(new WeakReference<Context>(RuntimeEnvironment.application))
                .when(windowAndroid)
                .getContext();

        // Create formatter mocks
        UrlFormatter.Natives urlFormatterJniMock = Mockito.mock(UrlFormatter.Natives.class);
        mJniMocker.mock(UrlFormatterJni.TEST_HOOKS, urlFormatterJniMock);
        Mockito.doReturn("example.test")
                .when(urlFormatterJniMock)
                .formatStringUrlForSecurityDisplay(
                        Mockito.any(), Mockito.eq(SchemeDisplay.OMIT_CRYPTOGRAPHIC));

        mResponseCallback =
                () -> {
                    mUserResponded = true;
                };
        mOptOutCallback =
                () -> {
                    mUserOptedOut = true;
                };

        ShadowBottomSheetControllerProvider.setBottomSheetController(
                createBottomSheetController(/* requestShowContentResponse= */ true));

        mClock = new FakeClock();
    }

    @After
    public void tearDown() {
        if (mNoMatchingCredController != null) mNoMatchingCredController.close();
    }

    private void createNoMatchingCredController() {
        mNoMatchingCredController =
                SecurePaymentConfirmationNoMatchingCredController.create(mWebContents);
        // Some tests expect a null controller, e.g. for a null web contents.
        if (mNoMatchingCredController != null) {
            mNoMatchingCredController.setInputProtectorForTesting(new InputProtector(mClock));
        }
    }

    private BottomSheetController createBottomSheetController(boolean requestShowContentResponse) {
        BottomSheetController controller = Mockito.mock(BottomSheetController.class);
        Mockito.doReturn(requestShowContentResponse)
                .when(controller)
                .requestShowContent(Mockito.any(BottomSheetContent.class), Mockito.anyBoolean());
        return controller;
    }

    private boolean show() {
        return show(/* enableOptOut= */ false);
    }

    private boolean show(boolean enableOptOut) {
        if (mNoMatchingCredController == null) return false;

        mUserResponded = false;
        mUserOptedOut = false;

        String rpId = "rp.example";
        return mNoMatchingCredController.show(
                mResponseCallback, mOptOutCallback, enableOptOut, rpId);
    }

    private void setWindowAndroid(WindowAndroid windowAndroid, WebContents webContents) {
        Mockito.doReturn(windowAndroid).when(webContents).getTopLevelNativeWindow();
    }

    private void setContext(Context context) {
        WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
        Mockito.doReturn(new WeakReference<Context>(context)).when(windowAndroid).getContext();
    }

    @Test
    @Feature({"Payments"})
    public void testShow() {
        createNoMatchingCredController();
        show();
        SecurePaymentConfirmationNoMatchingCredView view = mNoMatchingCredController.getView();
        Assert.assertNotNull(view);
        Assert.assertTrue(view.mDescription.getText().toString().contains("example.test"));
        // Opt-out text should be hidden by default.
        Assert.assertEquals(View.GONE, view.mOptOutText.getVisibility());
    }

    @Test
    @Feature({"Payments"})
    public void testShowRendersOptOutWhenRequested() {
        createNoMatchingCredController();
        show(/* enableOptOut= */ true);
        SecurePaymentConfirmationNoMatchingCredView view = mNoMatchingCredController.getView();
        Assert.assertNotNull(view);
        Assert.assertEquals(View.VISIBLE, view.mOptOutText.getVisibility());
        Assert.assertTrue(view.mOptOutText.getText().toString().contains("rp.example"));
    }

    @Test
    @Feature({"Payments"})
    public void testShowOnResponse() {
        createNoMatchingCredController();
        show();
        mClock.advanceCurrentTimeMillis(SAFE_INPUT_DELAY);
        mNoMatchingCredController.getView().mOkButton.performClick();
        Assert.assertTrue(mUserResponded);
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testShowOnOptOut() {
        createNoMatchingCredController();
        show(/* enableOptOut= */ true);
        mClock.advanceCurrentTimeMillis(SAFE_INPUT_DELAY);
        SecurePaymentConfirmationNoMatchingCredView credView = mNoMatchingCredController.getView();
        credView.mOptOutText.getClickableSpans()[0].onClick(credView.mOptOutText);
        Assert.assertTrue(mUserOptedOut);
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testUnintentedInput() {
        createNoMatchingCredController();
        show(/* enableOptOut= */ true);

        // Clicking immediately is prevented.
        mNoMatchingCredController.getView().mOkButton.performClick();
        Assert.assertFalse(mUserResponded);
        Assert.assertFalse(mNoMatchingCredController.isHidden());

        SecurePaymentConfirmationNoMatchingCredView credView = mNoMatchingCredController.getView();
        credView.mOptOutText.getClickableSpans()[0].onClick(credView.mOptOutText);
        Assert.assertFalse(mUserOptedOut);
        Assert.assertFalse(mNoMatchingCredController.isHidden());

        // Clicking after an interval less than the threshold is still prevented.
        mClock.advanceCurrentTimeMillis(IGNORED_INPUT_DELAY);

        mNoMatchingCredController.getView().mOkButton.performClick();
        Assert.assertFalse(mUserResponded);
        Assert.assertFalse(mNoMatchingCredController.isHidden());

        credView.mOptOutText.getClickableSpans()[0].onClick(credView.mOptOutText);
        Assert.assertFalse(mUserOptedOut);
        Assert.assertFalse(mNoMatchingCredController.isHidden());

        // Clicking confirm after the threshold is no longer prevented and closes the dialog.
        mClock.advanceCurrentTimeMillis(SAFE_INPUT_DELAY);
        mNoMatchingCredController.getView().mOkButton.performClick();
        Assert.assertTrue(mUserResponded);
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testRequestShowContentFalse() {
        createNoMatchingCredController();
        ShadowBottomSheetControllerProvider.setBottomSheetController(
                createBottomSheetController(/* requestShowContentResponse= */ false));
        Assert.assertFalse(show());
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testCreateWithNullWebContents() {
        mWebContents = null;
        createNoMatchingCredController();
        Assert.assertNull(mNoMatchingCredController);
    }

    @Test
    @Feature({"Payments"})
    public void testShowWithNullWindowAndroid() {
        setWindowAndroid(null, mWebContents);
        createNoMatchingCredController();
        Assert.assertFalse(show());
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testShowWithNullContext() {
        setContext(null);
        createNoMatchingCredController();
        Assert.assertFalse(show());
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testShowWithNullBottomSheetController() {
        ShadowBottomSheetControllerProvider.setBottomSheetController(null);
        createNoMatchingCredController();
        Assert.assertFalse(show());
        Assert.assertTrue(mNoMatchingCredController.isHidden());
    }

    @Test
    @Feature({"Payments"})
    public void testShowTwiceWithClose() {
        createNoMatchingCredController();
        Assert.assertTrue(show());
        mNoMatchingCredController.close();
        Assert.assertTrue(show());
    }

    @Test
    @Feature({"Payments"})
    public void testShowTwiceWithoutClose() {
        createNoMatchingCredController();
        Assert.assertTrue(show());
        Assert.assertFalse(show());
    }
}