Skip to content

Commit

Permalink
Integration test for local web approval flow.
Browse files Browse the repository at this point in the history
Bug: b/243916194
Change-Id: I7441065af970984a20f61221e1be7ec8572d89ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3968640
Reviewed-by: James Lee <ljjlee@google.com>
Commit-Queue: Anthi Orfanou <anthie@google.com>
Reviewed-by: Tanmoy Mollik <triploblastic@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1084885}
  • Loading branch information
anthie@google.com authored and Chromium LUCI CQ committed Dec 19, 2022
1 parent 1f144fd commit 473361d
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 124 deletions.
19 changes: 16 additions & 3 deletions chrome/browser/supervised_user/BUILD.gn
Expand Up @@ -104,6 +104,7 @@ if (is_android) {

annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
sources = [
"android/java/src/org/chromium/chrome/browser/supervised_user/ParentAuthDelegateProvider.java",
"android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java",
"android/java/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalCoordinator.java",
"android/java/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalMediator.java",
Expand All @@ -120,12 +121,12 @@ if (is_android) {

generate_jni("test_support_jni_headers") {
testonly = true
sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java" ]
sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java" ]
}

android_library("test_support_java") {
testonly = true
sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java" ]
sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java" ]
deps = [
":test_support_jni_headers",
"//base:jni_java",
Expand All @@ -140,23 +141,35 @@ if (is_android) {
android_library("javatests") {
testonly = true
sources = [
"android/java/src/org/chromium/chrome/browser/supervised_user/test/WebsiteParentApprovalTest.java",
"android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalTest.java",
"android/javatests/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalMetricsUnitTest.java",
"android/javatests/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalSheetContentUnitTest.java",
]
deps = [
":test_support_java",
"//base:base_java",
"//base:base_java_test_support",
"//chrome/android:chrome_java",
"//chrome/browser/flags:java",
"//chrome/browser/profiles/android:java",
"//chrome/browser/signin/services/android:java",
"//chrome/browser/supervised_user:parent_auth_delegate_java",
"//chrome/browser/supervised_user:supervised_user_metrics_java",
"//chrome/browser/supervised_user:website_parent_approval_java",
"//chrome/test/android:chrome_java_integration_test_support",
"//chrome/test/android:chrome_java_test_pagecontroller",
"//components/browser_ui/bottomsheet/android:java",
"//components/browser_ui/bottomsheet/android/test:java",
"//components/signin/public/android:java",
"//components/signin/public/android:signin_java_test_support",
"//content/public/test/android:content_java_test_support",
"//net/android:net_java_test_support",
"//third_party/android_deps:espresso_java",
"//third_party/androidx:androidx_test_runner_java",
"//third_party/junit:junit",
"//third_party/mockito:mockito_java",
"//ui/android:ui_java_test_support",
"//ui/android:ui_no_recycler_view_java",
"//url:gurl_java",
]
}
Expand Down
Expand Up @@ -11,6 +11,7 @@ found in the LICENSE file.
doesn't support dynamic colors. -->

<LinearLayout
android:id="@+id/local_parent_approval_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
Expand Down
@@ -0,0 +1,46 @@
// 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 androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ThreadUtils;

/**
* Helper class that provides a test or production instance for
* {@link ParentAuthDelegate}.
*/
public class ParentAuthDelegateProvider {
private static ParentAuthDelegate sInstance;
private static ParentAuthDelegate sTestingInstance;

/**
* Sets the test instance. Can be called multiple times to change the instance
* during testing.
*/
@VisibleForTesting
@AnyThread
public static void setInstanceForTests(ParentAuthDelegate parentAuthDelegate) {
// TODO(b/243916194): Change to the recommended alternative for deprecated method.
ThreadUtils.runOnUiThread(() -> { sTestingInstance = parentAuthDelegate; });
}

/**
* Returns singleton instance.
*/
@MainThread
public static ParentAuthDelegate getInstance() {
ThreadUtils.assertOnUiThread();
if (sTestingInstance != null) {
return sTestingInstance;
}
if (sInstance == null) {
sInstance = new ParentAuthDelegateImpl();
}
return sInstance;
}
}
@@ -1,8 +1,8 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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.test;
package org.chromium.chrome.browser.supervised_user;

import org.chromium.base.annotations.NativeMethods;
import org.chromium.chrome.browser.profiles.Profile;
Expand All @@ -13,7 +13,7 @@
*
* This should only be used in tests.
*/
public class SupervisedUserSettingsBridge {
class SupervisedUserSettingsBridge {
/** Set the website filtering behaviour for this user. */
static void setFilteringBehavior(Profile profile, @FilteringBehavior int setting) {
SupervisedUserSettingsBridgeJni.get().setFilteringBehavior(profile, setting);
Expand Down
Expand Up @@ -86,7 +86,7 @@ private static boolean isLocalApprovalSupported() {
@CalledByNative
private static void requestLocalApproval(WindowAndroid windowAndroid, GURL url) {
// First ask the parent to authenticate.
ParentAuthDelegate delegate = new ParentAuthDelegateImpl();
ParentAuthDelegate delegate = ParentAuthDelegateProvider.getInstance();
FaviconHelper faviconHelper = new FaviconHelper();
delegate.requestLocalAuth(windowAndroid, url,
(success) -> { onParentAuthComplete(success, windowAndroid, url, faviconHelper); });
Expand Down
@@ -0,0 +1,249 @@
// 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.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
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.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.Profile;
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.Features.EnableFeatures;
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.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.test.util.DisableAnimationsTestRule;
import org.chromium.ui.test.util.ViewUtils;
import org.chromium.url.GURL;

import java.util.concurrent.TimeoutException;

/**
* Tests the local website approval flow.
*/
@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})
@EnableFeatures(
{ChromeFeatureList.LOCAL_WEB_APPROVALS, ChromeFeatureList.WEB_FILTER_INTERSTITIAL_REFRESH})
public class WebsiteParentApprovalTest {
// TODO(b/243916194): Expand the test coverage beyond the completion callback, up to the page
// refresh.
// (TODO b/243916194): Expand test until the metric collection step on the native side. Requires
// not mocking the natives so that the flow moves onto their real execution.

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);
@Rule
public final DisableAnimationsTestRule sDisableAnimationsRule = new DisableAnimationsTestRule();

private static final String TEST_PAGE = "/chrome/test/data/android/about.html";
private static final String LOCAL_APPROVALS_BUTTON_NODE_ID = "local-approvals-button";

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();
TestThreadUtils.runOnUiThreadBlocking(() -> {
ChromeTabbedActivity activity = mTabbedActivityTestRule.getActivity();
mBottomSheetController =
activity.getRootUiCoordinatorForTesting().getBottomSheetController();
mBottomSheetTestSupport = new BottomSheetTestSupport(mBottomSheetController);
});

mSigninTestRule.addChildTestAccountThenWaitForSignin();
TestThreadUtils.runOnUiThreadBlocking(() -> {
SupervisedUserSettingsBridge.setFilteringBehavior(
Profile.getLastUsedRegularProfile(), FilteringBehavior.BLOCK);
});
mWebContents = mTabbedActivityTestRule.getWebContents();

mocker.mock(WebsiteParentApprovalJni.TEST_HOOKS, mWebsiteParentApprovalNativesMock);
doNothing()
.when(mWebsiteParentApprovalNativesMock)
.fetchFavicon(any(GURL.class), any(Integer.class), any(Integer.class),
any(Callback.class));
//@TODO b:243916194 : Trigger the execution of the real (void) method.
doNothing().when(mWebsiteParentApprovalNativesMock).onCompletion(any(Integer.class));

// @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 clickAskInPerson() {
try {
String contents =
DOMUtils.getNodeContents(mWebContents, LOCAL_APPROVALS_BUTTON_NODE_ID);
} catch (TimeoutException e) {
throw new RuntimeException("Local approval button not found");
}
DOMUtils.clickNodeWithJavaScript(mWebContents, LOCAL_APPROVALS_BUTTON_NODE_ID);
}

private void checkParentApprovalBottomSheetVisible() {
onView(isRoot()).check(ViewUtils.waitForView(
withId(R.id.local_parent_approval_layout), ViewUtils.VIEW_VISIBLE));
// Ensure all animations have ended before allowing interaction with the view.
TestThreadUtils.runOnUiThreadBlocking(
() -> { mBottomSheetTestSupport.endAllAnimations(); });
}

private void clickApprove() {
checkParentApprovalBottomSheetVisible();
onView(withId(R.id.approve_button))
.check(matches(isCompletelyDisplayed()))
.perform(click());
}

private void clickDoNotApprove() {
checkParentApprovalBottomSheetVisible();
onView(withId(R.id.deny_button)).check(matches(isCompletelyDisplayed())).perform(click());
}

private void checkParentApprovalScreenClosedAfterClick() {
// Ensure all animations have ended. Otherwise the following check may fail.
TestThreadUtils.runOnUiThreadBlocking(
() -> { mBottomSheetTestSupport.endAllAnimations(); });
onView(isRoot()).check(ViewUtils.waitForView(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);

clickAskInPerson();
clickApprove();

checkParentApprovalScreenClosedAfterClick();
}

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

clickAskInPerson();
clickDoNotApprove();

checkParentApprovalScreenClosedAfterClick();
}

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

clickAskInPerson();
clickApprove();

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

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

clickAskInPerson();
clickDoNotApprove();

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

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

clickAskInPerson();

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

0 comments on commit 473361d

Please sign in to comment.