// Copyright 2020 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.firstrun;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.chromium.ui.test.util.MockitoHelper.doCallback;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.enterprise.util.EnterpriseInfo;
import org.chromium.chrome.browser.enterprise.util.EnterpriseInfo.OwnedState;
import org.chromium.components.policy.PolicyService;
/**
* Unit tests for {@link SkipTosDialogPolicyListener}.
*
* <p>For simplicity, this test will not cover cases that already tests in base class unit test
* {@link PolicyLoadListenerUnitTest}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
shadows = {SkipTosDialogPolicyListenerUnitTest.ShadowFirstRunUtils.class})
// TODO(crbug.com/40182398): Rewrite using paused loop. See crbug for details.
@LooperMode(LooperMode.Mode.LEGACY)
public class SkipTosDialogPolicyListenerUnitTest {
private static final String HIST_IS_DEVICE_OWNED_DETECTED =
"histogramRecorded.OnIsDeviceOwnedDetected";
private static final String HIST_POLICY_LOAD_LISTENER_AVAILABLE =
"histogramRecorded.OnPolicyLoadListenerAvailable";
@Implements(FirstRunUtils.class)
static class ShadowFirstRunUtils {
static boolean sIsCctTosDialogEnabled;
@Implementation
public static boolean isCctTosDialogEnabled() {
return sIsCctTosDialogEnabled;
}
}
static class TestHistNameProvider implements SkipTosDialogPolicyListener.HistogramNameProvider {
String mHistogramForEnterpriseInfo;
String mHistogramForPolicyLoadListener;
public TestHistNameProvider() {
mHistogramForEnterpriseInfo = HIST_IS_DEVICE_OWNED_DETECTED;
mHistogramForPolicyLoadListener = HIST_POLICY_LOAD_LISTENER_AVAILABLE;
}
@Override
public String getOnDeviceOwnedDetectedTimeHistogramName() {
return mHistogramForEnterpriseInfo;
}
@Override
public String getOnPolicyAvailableTimeHistogramName() {
return mHistogramForPolicyLoadListener;
}
}
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Spy public Callback<Boolean> mTosDialogCallback;
@Spy public TestHistNameProvider mHistogramNameProvider;
@Mock public OneshotSupplier<Boolean> mMockPolicyLoadListener;
@Mock public EnterpriseInfo mMockEnterpriseInfo;
private SkipTosDialogPolicyListener mSkipTosDialogPolicyListener;
private Callback<OwnedState> mEnterpriseInfoCallback;
private Callback<Boolean> mPolicyLoadListenerCallback;
@Before
public void setUp() {
doCallback((Callback<OwnedState> callback) -> mEnterpriseInfoCallback = callback)
.when(mMockEnterpriseInfo)
.getDeviceEnterpriseInfo(any());
doCallback((Callback<Boolean> callback) -> mPolicyLoadListenerCallback = callback)
.when(mMockPolicyLoadListener)
.onAvailable(any());
// Set ToS to enabled by default.
ShadowFirstRunUtils.sIsCctTosDialogEnabled = true;
buildNewSkipTosDialogPolicyListener();
assertPolicyCheckNotComplete();
Mockito.verify(mMockEnterpriseInfo).getDeviceEnterpriseInfo(mEnterpriseInfoCallback);
Mockito.verify(mMockPolicyLoadListener).onAvailable(mPolicyLoadListenerCallback);
}
@Test
public void testDeviceNotOwned() {
setDeviceFullyManaged(false);
assertTosDialogEnabled();
assertHistogramsRecorded(true, false);
}
@Test
public void testNoPolicyLoaded() {
mPolicyLoadListenerCallback.onResult(false);
assertTosDialogEnabled();
assertHistogramsRecorded(false, false);
}
@Test
public void testPolicyLoadedWithNoEffect() {
ShadowFirstRunUtils.sIsCctTosDialogEnabled = true;
mPolicyLoadListenerCallback.onResult(true);
assertTosDialogEnabled();
assertHistogramsRecorded(false, true);
}
@Test
public void testDeviceOwnedWithoutPolicySet() {
setDeviceFullyManaged(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(true, false);
mPolicyLoadListenerCallback.onResult(false);
assertTosDialogEnabled();
assertHistogramsRecorded(true, false);
}
@Test
public void testDeviceOwnedWithPolicySetToNoEffect() {
setDeviceFullyManaged(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(true, false);
mPolicyLoadListenerCallback.onResult(true);
assertTosDialogEnabled();
assertHistogramsRecorded(true, true);
}
@Test
public void testDeviceOwnedWithPolicySetToSkip() {
setDeviceFullyManaged(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(true, false);
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
mPolicyLoadListenerCallback.onResult(true);
assertTosDialogSkipped();
assertHistogramsRecorded(true, true);
}
@Test
public void testPolicySetToSkipWithDeviceNotOwned() {
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
mPolicyLoadListenerCallback.onResult(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, true);
setDeviceFullyManaged(false);
assertTosDialogEnabled();
assertHistogramsRecorded(true, true);
}
@Test
public void testPolicySetToSkipWithDeviceOwned() {
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
mPolicyLoadListenerCallback.onResult(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, true);
setDeviceFullyManaged(true);
assertTosDialogSkipped();
assertHistogramsRecorded(true, true);
}
@Test
public void testDestroy_PolicyNotFound() {
mSkipTosDialogPolicyListener.destroy();
// Policy signal should be ignored after destroy.
mPolicyLoadListenerCallback.onResult(false);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, false);
}
@Test
public void testDestroy_DeviceNotOwned() {
mSkipTosDialogPolicyListener.destroy();
// Device owned signal should be ignored after destroy.
setDeviceFullyManaged(false);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, false);
}
@Test
public void testDestroy_SkipTosDialog() {
mSkipTosDialogPolicyListener.destroy();
setDeviceFullyManaged(true);
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, false);
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
mPolicyLoadListenerCallback.onResult(true);
// Signals should be ignore since #destroy happened.
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, false);
}
@Test
public void testDestroy_WithOutstandingOnAvailable() {
// Inspired by a crash in https://crbug.com/1200979.
CallbackHelper onAvailabileCallbackHelper = new CallbackHelper();
mSkipTosDialogPolicyListener.onAvailable((b) -> onAvailabileCallbackHelper.notifyCalled());
// While #onResult would normally result in the #onAvailable callback being run, the
// callback is actually posted to a Handler and run asynchronously. Robolectric typically
// runs all callbacks synchronously, so pause the ShadowLooper to stop this.
ShadowLooper.pauseMainLooper();
mPolicyLoadListenerCallback.onResult(false);
Assert.assertEquals(0, onAvailabileCallbackHelper.getCallCount());
// Now call #destroy() which means the #onAvailable should never be run, on which our
// callers assume/depend. #unPauseMainLooper() will cause anything posted to Handlers to be
// run synchronously, after which it is safe for us to check/assert.
mSkipTosDialogPolicyListener.destroy();
ShadowLooper.unPauseMainLooper();
Assert.assertEquals(0, onAvailabileCallbackHelper.getCallCount());
}
@Test
public void testBuildListenerAfterPolicyLoadedAsNotNeeded() {
setupMockPolicyLoadListenerInitialized(false);
buildNewSkipTosDialogPolicyListener();
assertTosDialogEnabled();
assertHistogramsRecorded(false, false);
}
@Test
public void testBuildListenerAfterPolicyLoadedAsNeeded_TosSkipped() {
setupMockPolicyLoadListenerInitialized(true);
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
buildNewSkipTosDialogPolicyListener();
assertPolicyCheckNotComplete();
assertHistogramsRecorded(false, true);
setDeviceFullyManaged(true);
assertTosDialogSkipped();
assertHistogramsRecorded(true, true);
}
@Test
public void testBuildListenerAfterPolicyLoadedAsNeeded_TosEnabled() {
setupMockPolicyLoadListenerInitialized(true);
buildNewSkipTosDialogPolicyListener();
assertTosDialogEnabled();
assertHistogramsRecorded(false, true);
}
@Test
public void testBuildListenerAfterDetectedDeviceNotManaged() {
setupMockEnterpriseInitializedWithDeviceManaged(false);
buildNewSkipTosDialogPolicyListener();
assertTosDialogEnabled();
assertHistogramsRecorded(true, false);
}
@Test
public void testBuildListenerAfterDetectedDeviceManaged() {
setupMockEnterpriseInitializedWithDeviceManaged(true);
buildNewSkipTosDialogPolicyListener();
assertPolicyCheckNotComplete();
assertHistogramsRecorded(true, false);
ShadowFirstRunUtils.sIsCctTosDialogEnabled = false;
mPolicyLoadListenerCallback.onResult(true);
assertTosDialogSkipped();
assertHistogramsRecorded(true, true);
}
@Test
public void testHistogramNameProvider_UpdateProvider() {
// Update the names for mHistogramNameProvider and test if the old hists are not recorded.
String newHistogramForEnterprise = "another.histogram.enterprise";
String newHistogramForPolicy = "another.histogram.policy";
mHistogramNameProvider.mHistogramForEnterpriseInfo = newHistogramForEnterprise;
mHistogramNameProvider.mHistogramForPolicyLoadListener = newHistogramForPolicy;
setDeviceFullyManaged(true);
Mockito.verify(mHistogramNameProvider).getOnDeviceOwnedDetectedTimeHistogramName();
Assert.assertEquals(
"Old histogram for EnterpriseInfo should not be recorded.",
0,
RecordHistogram.getHistogramTotalCountForTesting(HIST_IS_DEVICE_OWNED_DETECTED));
Assert.assertEquals(
"New Histogram for EnterpriseInfo should be recorded.",
1,
RecordHistogram.getHistogramTotalCountForTesting(newHistogramForEnterprise));
mPolicyLoadListenerCallback.onResult(true);
Mockito.verify(mHistogramNameProvider).getOnPolicyAvailableTimeHistogramName();
Assert.assertEquals(
"Old histogram for Policy should not be recorded.",
0,
RecordHistogram.getHistogramTotalCountForTesting(
HIST_POLICY_LOAD_LISTENER_AVAILABLE));
Assert.assertEquals(
"New Histogram for Policy should be recorded.",
1,
RecordHistogram.getHistogramTotalCountForTesting(newHistogramForPolicy));
}
@Test
public void testHistogramNameProvider_NoProvider() {
buildNewSkipTosDialogPolicyListenerWithHistogram(false);
setDeviceFullyManaged(true);
Assert.assertEquals(
"No histogram for EnterpriseInfo should not be recorded.",
0,
RecordHistogram.getHistogramTotalCountForTesting(HIST_IS_DEVICE_OWNED_DETECTED));
mPolicyLoadListenerCallback.onResult(true);
Assert.assertEquals(
"No histogram for Policy should not be recorded.",
0,
RecordHistogram.getHistogramTotalCountForTesting(
HIST_POLICY_LOAD_LISTENER_AVAILABLE));
}
@Test
public void testCreateAndOwnPolicyLoadListener()
throws NoSuchFieldException, IllegalAccessException {
FirstRunAppRestrictionInfo mockAppRestrictionInfo =
Mockito.mock(FirstRunAppRestrictionInfo.class);
OneshotSupplier<PolicyService> mockSupplier =
(OneshotSupplier<PolicyService>) Mockito.mock(OneshotSupplier.class);
SkipTosDialogPolicyListener targetListener =
new SkipTosDialogPolicyListener(
mockAppRestrictionInfo, mockSupplier, mMockEnterpriseInfo, null);
Assert.assertNotNull(
"SkipTosDialogPolicyListener should create and own a PolicyLoadListener.",
targetListener.getPolicyLoadListenerForTesting());
PolicyLoadListener spyListener =
Mockito.spy(targetListener.getPolicyLoadListenerForTesting());
var field = SkipTosDialogPolicyListener.class.getDeclaredField("mPolicyLoadListener");
field.setAccessible(true);
field.set(targetListener, spyListener);
targetListener.destroy();
Mockito.verify(spyListener).destroy();
}
private void assertTosDialogEnabled() {
Assert.assertFalse("ToS dialog should be enabled.", mSkipTosDialogPolicyListener.get());
Mockito.verify(mTosDialogCallback).onResult(false);
}
private void assertTosDialogSkipped() {
Assert.assertTrue(
"ToS dialog should be skipped according to device and enterprise setting.",
mSkipTosDialogPolicyListener.get());
Mockito.verify(mTosDialogCallback).onResult(true);
}
private void assertPolicyCheckNotComplete() {
Assert.assertNull(
"Whether ToS policy might take effect should not be decided yet.",
mSkipTosDialogPolicyListener.get());
Mockito.verify(mTosDialogCallback, never()).onResult(anyBoolean());
}
private void assertHistogramsRecorded(boolean isDeviceOwned, boolean isPolicyAvailable) {
assertOnDeviceOwnedDetectedTimeHistogramRecorded(isDeviceOwned);
assertOnPolicyAvailableTimeHistogramRecorded(isPolicyAvailable);
}
private void assertOnDeviceOwnedDetectedTimeHistogramRecorded(boolean isRecorded) {
int timesRecorded = isRecorded ? 1 : 0;
Mockito.verify(mHistogramNameProvider, times(timesRecorded))
.getOnDeviceOwnedDetectedTimeHistogramName();
Assert.assertEquals(
"Histogram for EnterpriseInfo is not recorded correctly.",
timesRecorded,
RecordHistogram.getHistogramTotalCountForTesting(HIST_IS_DEVICE_OWNED_DETECTED));
}
private void assertOnPolicyAvailableTimeHistogramRecorded(boolean isRecorded) {
int timesRecorded = isRecorded ? 1 : 0;
Mockito.verify(mHistogramNameProvider, times(timesRecorded))
.getOnPolicyAvailableTimeHistogramName();
Assert.assertEquals(
"Histogram for PolicyLoadListener is not recorded.",
timesRecorded,
RecordHistogram.getHistogramTotalCountForTesting(
HIST_POLICY_LOAD_LISTENER_AVAILABLE));
}
private void buildNewSkipTosDialogPolicyListener() {
buildNewSkipTosDialogPolicyListenerWithHistogram(true);
}
private void buildNewSkipTosDialogPolicyListenerWithHistogram(boolean reportHistogram) {
mSkipTosDialogPolicyListener =
new SkipTosDialogPolicyListener(
mMockPolicyLoadListener,
mMockEnterpriseInfo,
reportHistogram ? mHistogramNameProvider : null);
mSkipTosDialogPolicyListener.onAvailable(mTosDialogCallback);
}
private void setupMockPolicyLoadListenerInitialized(boolean hasPolicy) {
Mockito.reset(mMockPolicyLoadListener);
mPolicyLoadListenerCallback = null;
Mockito.doAnswer(
invocation -> {
Callback<Boolean> callback = invocation.getArgument(0);
mPolicyLoadListenerCallback = callback;
callback.onResult(hasPolicy);
return hasPolicy;
})
.when(mMockPolicyLoadListener)
.onAvailable(any());
}
private void setupMockEnterpriseInitializedWithDeviceManaged(boolean isDeviceOwned) {
Mockito.reset(mMockEnterpriseInfo);
mEnterpriseInfoCallback = null;
Mockito.doAnswer(
invocation -> {
Callback<OwnedState> callback = invocation.getArgument(0);
mEnterpriseInfoCallback = callback;
OwnedState state = new OwnedState(isDeviceOwned, false);
callback.onResult(state);
return state;
})
.when(mMockEnterpriseInfo)
.getDeviceEnterpriseInfo(any());
}
private void setDeviceFullyManaged(boolean isDeviceOwned) {
mEnterpriseInfoCallback.onResult(new OwnedState(isDeviceOwned, false));
}
}