| // Copyright 2018 The Feed Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.android.libraries.feed.feedactionreader; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Matchers.anyList; |
| import static org.mockito.Matchers.anyListOf; |
| import static org.mockito.Matchers.eq; |
| import static org.mockito.Matchers.same; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| import static org.mockito.MockitoAnnotations.initMocks; |
| |
| import com.google.android.libraries.feed.api.common.DismissActionWithSemanticProperties; |
| import com.google.android.libraries.feed.api.common.SemanticPropertiesWithId; |
| import com.google.android.libraries.feed.api.common.testing.ContentIdGenerators; |
| import com.google.android.libraries.feed.api.protocoladapter.ProtocolAdapter; |
| import com.google.android.libraries.feed.api.store.LocalActionMutation.ActionType; |
| import com.google.android.libraries.feed.api.store.Store; |
| import com.google.android.libraries.feed.common.Result; |
| import com.google.android.libraries.feed.common.concurrent.testing.FakeTaskQueue; |
| import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils; |
| import com.google.android.libraries.feed.common.testing.WireProtocolResponseBuilder; |
| import com.google.android.libraries.feed.common.time.testing.FakeClock; |
| import com.google.android.libraries.feed.host.config.Configuration; |
| import com.google.android.libraries.feed.host.config.Configuration.ConfigKey; |
| import com.google.android.libraries.feed.internalapi.actionmanager.ActionReader; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.search.now.feed.client.StreamDataProto.StreamLocalAction; |
| import com.google.search.now.wire.feed.ContentIdProto.ContentId; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.robolectric.RobolectricTestRunner; |
| |
| /** Tests of the {@link FeedActionReader} class. */ |
| @RunWith(RobolectricTestRunner.class) |
| public class FeedActionReaderTest { |
| |
| private static final ContentIdGenerators ID_GENERATOR = new ContentIdGenerators(); |
| |
| private static final ContentId CONTENT_ID = WireProtocolResponseBuilder.createFeatureContentId(1); |
| private static final String CONTENT_ID_STRING = ID_GENERATOR.createContentId(CONTENT_ID); |
| private static final ContentId CONTENT_ID_2 = |
| WireProtocolResponseBuilder.createFeatureContentId(2); |
| private static final String CONTENT_ID_STRING_2 = ID_GENERATOR.createContentId(CONTENT_ID_2); |
| private static final long DEFAULT_TIME = TimeUnit.DAYS.toSeconds(42); |
| |
| private final FakeClock fakeClock = new FakeClock(); |
| private final FakeThreadUtils fakeThreadUtils = new FakeThreadUtils(); |
| |
| @Mock private Store store; |
| @Mock private ProtocolAdapter protocolAdapter; |
| @Mock private Configuration configuration; |
| |
| private ActionReader actionReader; |
| |
| @Before |
| public void setUp() throws Exception { |
| initMocks(this); |
| |
| when(configuration.getValueOrDefault(same(ConfigKey.DEFAULT_ACTION_TTL_SECONDS), any())) |
| .thenReturn(TimeUnit.DAYS.toSeconds(3)); |
| when(configuration.getValueOrDefault(same(ConfigKey.MINIMUM_VALID_ACTION_RATIO), any())) |
| .thenReturn(0.9); |
| |
| when(store.triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class))) |
| .thenReturn(() -> {}); |
| |
| actionReader = |
| new FeedActionReader(store, fakeClock, protocolAdapter, getTaskQueue(), configuration); |
| |
| when(protocolAdapter.getWireContentId(CONTENT_ID_STRING)) |
| .thenReturn(Result.success(CONTENT_ID)); |
| when(protocolAdapter.getWireContentId(CONTENT_ID_STRING_2)) |
| .thenReturn(Result.success(CONTENT_ID_2)); |
| } |
| |
| @Test |
| public void getAllDismissedActions() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| mockStoreCalls(Collections.singletonList(dismissAction), Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| assertThat(dismissActions) |
| .containsExactly(new DismissActionWithSemanticProperties(CONTENT_ID, null)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_empty() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| mockStoreCalls(Collections.emptyList(), Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| assertThat(dismissActions).hasSize(0); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_storeError_getAllDismissActions() { |
| fakeClock.set(DEFAULT_TIME); |
| when(store.getAllDismissLocalActions()).thenReturn(Result.failure()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isFalse(); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_storeError_getSemanticProperties() { |
| fakeClock.set(DEFAULT_TIME); |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| when(store.getAllDismissLocalActions()) |
| .thenReturn(Result.success(Collections.singletonList(dismissAction))); |
| when(store.getSemanticProperties(anyList())).thenReturn(Result.failure()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isFalse(); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_expired() { |
| fakeClock.set(TimeUnit.SECONDS.toMillis(DEFAULT_TIME) + TimeUnit.DAYS.toMillis(3)); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| List<StreamLocalAction> dismissActions = Collections.singletonList(dismissAction); |
| mockStoreCalls(dismissActions, Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| assertThat(dismissActionsResult.getValue()).hasSize(0); |
| verify(store).triggerLocalActionGc(eq(dismissActions), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_semanticProperties() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| byte[] semanticData = {12, 41}; |
| mockStoreCalls( |
| Collections.singletonList(dismissAction), |
| Collections.singletonList(new SemanticPropertiesWithId(CONTENT_ID_STRING, semanticData))); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| assertThat(dismissActions) |
| .containsExactly(new DismissActionWithSemanticProperties(CONTENT_ID, semanticData)); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_multipleActions() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| StreamLocalAction dismissAction2 = buildDismissAction(CONTENT_ID_STRING_2); |
| mockStoreCalls(Arrays.asList(dismissAction, dismissAction2), Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| |
| assertThat(dismissActions) |
| .containsExactly( |
| new DismissActionWithSemanticProperties(CONTENT_ID, null), |
| new DismissActionWithSemanticProperties(CONTENT_ID_2, null)); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_multipleActions_semanticProperties() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| StreamLocalAction dismissAction2 = buildDismissAction(CONTENT_ID_STRING_2); |
| byte[] semanticData = {12, 41}; |
| byte[] semanticData2 = {42, 72}; |
| mockStoreCalls( |
| Arrays.asList(dismissAction, dismissAction2), |
| Arrays.asList( |
| new SemanticPropertiesWithId(CONTENT_ID_STRING, semanticData), |
| new SemanticPropertiesWithId(CONTENT_ID_STRING_2, semanticData2))); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| |
| assertThat(dismissActions) |
| .containsExactly( |
| new DismissActionWithSemanticProperties(CONTENT_ID, semanticData), |
| new DismissActionWithSemanticProperties(CONTENT_ID_2, semanticData2)); |
| verify(store, never()) |
| .triggerLocalActionGc(anyListOf(StreamLocalAction.class), anyListOf(String.class)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_multipleActions_someExpired() { |
| fakeClock.set(TimeUnit.SECONDS.toMillis(DEFAULT_TIME) + TimeUnit.DAYS.toMillis(3)); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| StreamLocalAction dismissAction2 = |
| StreamLocalAction.newBuilder() |
| .setAction(ActionType.DISMISS) |
| .setFeatureContentId(CONTENT_ID_STRING_2) |
| .setTimestampSeconds(DEFAULT_TIME + TimeUnit.DAYS.toSeconds(2)) |
| .build(); |
| mockStoreCalls(Arrays.asList(dismissAction, dismissAction2), Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| |
| assertThat(dismissActionsResult.getValue()) |
| .containsExactly(new DismissActionWithSemanticProperties(CONTENT_ID_2, null)); |
| verify(store) |
| .triggerLocalActionGc( |
| Arrays.asList(dismissAction, dismissAction2), |
| Collections.singletonList(CONTENT_ID_STRING_2)); |
| } |
| |
| @Test |
| public void getAllDismissedActions_duplicateActions() { |
| fakeClock.set(DEFAULT_TIME); |
| |
| StreamLocalAction dismissAction = buildDismissAction(CONTENT_ID_STRING); |
| StreamLocalAction dismissAction2 = buildDismissAction(CONTENT_ID_STRING); |
| mockStoreCalls(Arrays.asList(dismissAction, dismissAction2), Collections.emptyList()); |
| |
| Result<List<DismissActionWithSemanticProperties>> dismissActionsResult = |
| actionReader.getDismissActionsWithSemanticProperties(); |
| assertThat(dismissActionsResult.isSuccessful()).isTrue(); |
| List<DismissActionWithSemanticProperties> dismissActions = dismissActionsResult.getValue(); |
| |
| assertThat(dismissActions).hasSize(1); |
| assertThat(dismissActions) |
| .containsExactly(new DismissActionWithSemanticProperties(CONTENT_ID, null)); |
| } |
| |
| private StreamLocalAction buildDismissAction(String contentId) { |
| return StreamLocalAction.newBuilder() |
| .setAction(ActionType.DISMISS) |
| .setFeatureContentId(contentId) |
| .setTimestampSeconds(DEFAULT_TIME) |
| .build(); |
| } |
| |
| private void mockStoreCalls( |
| List<StreamLocalAction> dismissActions, List<SemanticPropertiesWithId> semanticProperties) { |
| when(store.getAllDismissLocalActions()).thenReturn(Result.success(dismissActions)); |
| when(store.getSemanticProperties(anyList())).thenReturn(Result.success(semanticProperties)); |
| } |
| |
| private FakeTaskQueue getTaskQueue() { |
| FakeTaskQueue fakeTaskQueue = |
| new FakeTaskQueue(MoreExecutors.directExecutor(), fakeClock, fakeThreadUtils); |
| fakeTaskQueue.initialize(() -> {}); |
| return fakeTaskQueue; |
| } |
| } |