// 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.optimization_guide;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.util.Base64;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
import com.google.protobuf.ByteString;
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.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.FeatureList;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.components.optimization_guide.proto.CommonTypesProto.Any;
import org.chromium.components.optimization_guide.proto.HintsProto.KeyRepresentation;
import org.chromium.components.optimization_guide.proto.HintsProto.OptimizationType;
import org.chromium.components.optimization_guide.proto.PushNotificationProto.HintNotificationPayload;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
/** Unit tests for OptimizationGuidePushNotificationManager. */
@RunWith(BaseJUnit4ClassRunner.class)
// Batch this per class since the test is setting global feature state.
@Batch(Batch.PER_CLASS)
@EnableFeatures(ChromeFeatureList.OPTIMIZATION_GUIDE_PUSH_NOTIFICATIONS)
public class OptimizationGuidePushNotificationManagerUnitTest {
@Rule public JniMocker mocker = new JniMocker();
@Mock private Profile mProfile;
@Mock private OptimizationGuideBridgeFactory.Natives mOptimizationGuideBridgeFactoryJniMock;
@Mock private OptimizationGuideBridge mOptimizationGuideBridge;
private static final HintNotificationPayload NOTIFICATION_WITH_PAYLOAD =
HintNotificationPayload.newBuilder()
.setOptimizationType(OptimizationType.PERFORMANCE_HINTS)
.setKeyRepresentation(KeyRepresentation.FULL_URL)
.setHintKey("Testing")
.setPayload(
Any.newBuilder()
.setTypeUrl("com.testing")
.setValue(ByteString.copyFrom(new byte[] {0, 1, 2, 3, 4}))
.build())
.build();
private static final HintNotificationPayload NOTIFICATION_WITHOUT_PAYLOAD =
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD).clearPayload().build();
@Before
public void setUp() {
resetFeatureFlags();
MockitoAnnotations.initMocks(this);
mocker.mock(
org.chromium.chrome.browser.optimization_guide.OptimizationGuideBridgeFactoryJni
.TEST_HOOKS,
mOptimizationGuideBridgeFactoryJniMock);
doReturn(mOptimizationGuideBridge)
.when(mOptimizationGuideBridgeFactoryJniMock)
.getForProfile(mProfile);
ProfileManager.setLastUsedProfileForTesting(mProfile);
NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
}
@After
public void tearDown() {
resetFeatureFlags();
}
public void resetFeatureFlags() {
OptimizationGuidePushNotificationManager.clearCacheForAllTypes();
FeatureList.setTestFeatures(null);
}
@Test
@SmallTest
public void testBasicSuccessCaseNoNative() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITH_PAYLOAD);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager
.getOptTypesThatOverflowedPushNotifications());
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(1, cached.length);
Assert.assertEquals(NOTIFICATION_WITHOUT_PAYLOAD, cached[0]);
// There should not be notifications for other types.
Assert.assertEquals(
0,
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.LITE_PAGE)
.length);
Assert.assertEquals(
Arrays.asList(OptimizationType.PERFORMANCE_HINTS),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
OptimizationGuidePushNotificationManager.clearCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
}
@Test
@SmallTest
@UiThreadTest
public void testNativeCalled() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(true);
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITHOUT_PAYLOAD);
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
verify(mOptimizationGuideBridge, times(1))
.onNewPushNotification(eq(NOTIFICATION_WITHOUT_PAYLOAD));
}
@Test
@SmallTest
@DisableFeatures(ChromeFeatureList.OPTIMIZATION_GUIDE_PUSH_NOTIFICATIONS)
public void testFeatureDisabled() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITH_PAYLOAD);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager
.getOptTypesThatOverflowedPushNotifications());
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testClearAllOnFeatureOff() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.setOptimizationType(OptimizationType.LITE_PAGE)
.build());
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.setOptimizationType(OptimizationType.LITE_VIDEO)
.build());
Assert.assertEquals(
1,
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.LITE_PAGE)
.length);
Assert.assertEquals(
1,
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.LITE_VIDEO)
.length);
Assert.assertEquals(
Arrays.asList(OptimizationType.LITE_PAGE, OptimizationType.LITE_VIDEO),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
// Flag state cannot change within the same process instance, so this behavior does not
// actually get triggered in real usage.
ChromeFeatureList.sOptimizationGuidePushNotifications.setForTesting(false);
// Push another notification to trigger the clear.
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITH_PAYLOAD);
Assert.assertEquals(
0,
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.LITE_PAGE)
.length);
Assert.assertEquals(
0,
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.LITE_VIDEO)
.length);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testOverflow() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
final int overflowSize = 5;
OptimizationGuidePushNotificationManager.MAX_CACHE_SIZE.setForTesting(overflowSize);
for (int i = 1; i <= overflowSize; i++) {
Assert.assertEquals(
String.format("Iteration %d", i),
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager
.getOptTypesThatOverflowedPushNotifications());
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.setHintKey("hint " + i)
.build());
}
Assert.assertEquals(
Arrays.asList(OptimizationType.PERFORMANCE_HINTS),
OptimizationGuidePushNotificationManager
.getOptTypesThatOverflowedPushNotifications());
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNull(cached);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
OptimizationGuidePushNotificationManager.clearCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testIdenticalDeduplicated() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
for (int i = 0; i < 10; i++) {
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITH_PAYLOAD);
}
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager
.getOptTypesThatOverflowedPushNotifications());
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(1, cached.length);
Assert.assertEquals(NOTIFICATION_WITHOUT_PAYLOAD, cached[0]);
Assert.assertEquals(
Arrays.asList(OptimizationType.PERFORMANCE_HINTS),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testIncompleteNotPersisted() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
// No optimization type.
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.clearOptimizationType()
.build());
// No key representation.
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.clearKeyRepresentation()
.build());
// No hint key.
OptimizationGuidePushNotificationManager.onPushNotification(
HintNotificationPayload.newBuilder(NOTIFICATION_WITH_PAYLOAD)
.clearHintKey()
.build());
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
Assert.assertEquals(
new ArrayList<OptimizationType>(),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testPayloadOptional() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITHOUT_PAYLOAD);
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(1, cached.length);
Assert.assertEquals(NOTIFICATION_WITHOUT_PAYLOAD, cached[0]);
Assert.assertEquals(
Arrays.asList(OptimizationType.PERFORMANCE_HINTS),
OptimizationGuidePushNotificationManager.getOptTypesWithPushNotifications());
}
@Test
@SmallTest
public void testCacheDecodingErrors_Success() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
int startSuccessErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult", /* SUCCESS= */ 1);
int startTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
OptimizationGuidePushNotificationManager.onPushNotification(NOTIFICATION_WITHOUT_PAYLOAD);
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(1, cached.length);
Assert.assertEquals(NOTIFICATION_WITHOUT_PAYLOAD, cached[0]);
int afterSuccessErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult", /* SUCCESS= */ 1);
int afterTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
Assert.assertEquals(1, afterSuccessErrorCount - startSuccessErrorCount);
Assert.assertEquals(1, afterTotalCount - startTotalCount);
}
@Test
@SmallTest
public void testCacheDecodingErrors_InvalidProtobuf() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
int startPBErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult",
/* INVALID_PROTOBUF= */ 2);
int startTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
ChromeSharedPreferences.getInstance()
.writeStringSet(
OptimizationGuidePushNotificationManager.cacheKey(
OptimizationType.PERFORMANCE_HINTS),
new HashSet<String>(
Arrays.asList(
Base64.encodeToString(
new byte[] {1, 2, 3}, Base64.DEFAULT))));
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
int afterPBErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult",
/* INVALID_PROTOBUF= */ 2);
int afterTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
Assert.assertEquals(1, afterPBErrorCount - startPBErrorCount);
Assert.assertEquals(1, afterTotalCount - startTotalCount);
}
@Test
@SmallTest
public void testCacheDecodingErrors_Base64Error() {
OptimizationGuidePushNotificationManager.setNativeIsInitializedForTesting(false);
int startB64ErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult",
/* BASE64_ERROR= */ 3);
int startTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
ChromeSharedPreferences.getInstance()
.writeStringSet(
OptimizationGuidePushNotificationManager.cacheKey(
OptimizationType.PERFORMANCE_HINTS),
new HashSet<String>(Arrays.asList("=")));
HintNotificationPayload[] cached =
OptimizationGuidePushNotificationManager.getNotificationCacheForOptimizationType(
OptimizationType.PERFORMANCE_HINTS);
Assert.assertNotNull(cached);
Assert.assertEquals(0, cached.length);
int afterB64ErrorCount =
RecordHistogram.getHistogramValueCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult",
/* BASE64_ERROR= */ 3);
int afterTotalCount =
RecordHistogram.getHistogramTotalCountForTesting(
"OptimizationGuide.PushNotifications.ReadCacheResult");
Assert.assertEquals(1, afterB64ErrorCount - startB64ErrorCount);
Assert.assertEquals(1, afterTotalCount - startTotalCount);
}
}