blob: df3774906a70cb96b10f35a5d9c60a739dc03adc [file] [log] [blame]
// 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.feedrequestmanager;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.MockitoAnnotations.initMocks;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.Base64;
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.protoextensions.FeedExtensionRegistry;
import com.google.android.libraries.feed.common.testing.RequiredConsumer;
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.host.network.HttpRequest;
import com.google.android.libraries.feed.host.network.HttpRequest.HttpMethod;
import com.google.android.libraries.feed.host.network.HttpResponse;
import com.google.android.libraries.feed.testing.actionmanager.FakeActionReader;
import com.google.android.libraries.feed.testing.network.FakeNetworkClient;
import com.google.android.libraries.feed.testing.protocoladapter.FakeProtocolAdapter;
import com.google.android.libraries.feed.testing.store.FakeStore;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.search.now.feed.client.StreamDataProto.StreamUploadableAction;
import com.google.search.now.wire.feed.ActionRequestProto.ActionRequest;
import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
import com.google.search.now.wire.feed.FeedActionResponseProto.FeedActionResponse;
import com.google.search.now.wire.feed.FeedRequestProto.FeedRequest;
import com.google.search.now.wire.feed.OpaqueActionDataForTestProto.OpaqueActionDataForTest;
import com.google.search.now.wire.feed.OpaqueActionDataProto.OpaqueActionData;
import com.google.search.now.wire.feed.ResponseProto.Response;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.util.ReflectionHelpers;
/** Test of the {@link FeedActionUploadRequestManager} class. */
@RunWith(RobolectricTestRunner.class)
public class FeedActionUploadRequestManagerTest {
public static final long ID = 42;
private static final ConsistencyToken TOKEN_1 =
ConsistencyToken.newBuilder().setToken(ByteString.copyFrom(new byte[] {0x1, 0xa})).build();
private static final ConsistencyToken TOKEN_2 =
ConsistencyToken.newBuilder().setToken(ByteString.copyFrom(new byte[] {0x1, 0xf})).build();
private static final ConsistencyToken TOKEN_3 =
ConsistencyToken.newBuilder().setToken(ByteString.copyFrom(new byte[] {0x2, 0xa})).build();
private static final String CONTENT_ID = "contentId";
private static final String CONTENT_ID_2 = "contentId2";
private static final Response RESPONSE_1 =
Response.newBuilder()
.setExtension(
FeedActionResponse.feedActionResponse,
FeedActionResponse.newBuilder().setConsistencyToken(TOKEN_2).build())
.build();
private static final Response RESPONSE_2 =
Response.newBuilder()
.setExtension(
FeedActionResponse.feedActionResponse,
FeedActionResponse.newBuilder().setConsistencyToken(TOKEN_3).build())
.build();
private final Configuration configuration = new Configuration.Builder().build();
private final FakeClock fakeClock = new FakeClock();
private ExtensionRegistryLite registry;
private FakeActionReader fakeActionReader;
private FakeNetworkClient fakeNetworkClient;
private FakeProtocolAdapter fakeProtocolAdapter;
private FakeStore fakeStore;
private FakeTaskQueue fakeTaskQueue;
private FakeThreadUtils fakeThreadUtils;
private FeedActionUploadRequestManager requestManager;
private RequiredConsumer<Result<ConsistencyToken>> consumer;
@Before
public void setUp() {
initMocks(this);
registry = ExtensionRegistryLite.newInstance();
registry.add(FeedRequest.feedRequest);
fakeActionReader = new FakeActionReader();
fakeThreadUtils = FakeThreadUtils.withThreadChecks();
fakeNetworkClient = new FakeNetworkClient(fakeThreadUtils);
fakeTaskQueue = new FakeTaskQueue(MoreExecutors.directExecutor(), fakeClock, fakeThreadUtils);
fakeProtocolAdapter = new FakeProtocolAdapter();
fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
consumer =
new RequiredConsumer<>(
input -> {
fakeThreadUtils.checkNotMainThread();
});
ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", VERSION_CODES.KITKAT);
ReflectionHelpers.setStaticField(Build.VERSION.class, "RELEASE", "4.4.3");
ReflectionHelpers.setStaticField(Build.class, "CPU_ABI", "armeabi");
ReflectionHelpers.setStaticField(Build.class, "TAGS", "dev-keys");
requestManager = createRequestManager(configuration);
fakeThreadUtils.enforceMainThread(false);
fakeTaskQueue.initialize(() -> {});
}
@Test
public void testTriggerUploadActions_ttlExceededRemove() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 5)
.build();
requestManager = createRequestManager(configuration);
StreamUploadableAction action =
StreamUploadableAction.newBuilder()
.setUploadAttempts(1)
.setTimestampSeconds(1L)
.setFeatureContentId(CONTENT_ID)
.build();
fakeActionReader.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID));
fakeStore.setStreamUploadableActions(action);
fakeClock.set(5000L);
fakeNetworkClient.addResponse(
createHttpResponse(/* responseCode= */ 200, Response.getDefaultInstance()));
requestManager.triggerUploadActions(
setOf(action), ConsistencyToken.getDefaultInstance(), consumer);
assertThat(consumer.isCalled()).isTrue();
assertThat(fakeStore.getContentById(CONTENT_ID)).isEmpty();
}
@Test
public void testTriggerUploadActions_maxUploadsRemove() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.build();
requestManager = createRequestManager(configuration);
StreamUploadableAction action =
StreamUploadableAction.newBuilder()
.setUploadAttempts(2)
.setFeatureContentId(CONTENT_ID)
.build();
fakeActionReader.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID));
fakeNetworkClient.addResponse(
createHttpResponse(/* responseCode= */ 200, Response.getDefaultInstance()));
requestManager.triggerUploadActions(
setOf(action), ConsistencyToken.getDefaultInstance(), consumer);
assertThat(consumer.isCalled()).isTrue();
assertThat(fakeStore.getContentById(CONTENT_ID)).isEmpty();
}
@Test
public void testTriggerUploadActions_batchSuccess() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_SIZE_PER_REQUEST, 20)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_ACTIONS_PER_REQUEST, 2)
.build();
requestManager = createRequestManager(configuration);
Set<StreamUploadableAction> actionSet =
setOf(
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build(),
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID_2).build());
consumer =
new RequiredConsumer<>(
input -> {
fakeThreadUtils.checkNotMainThread();
assertThat(input.isSuccessful()).isTrue();
assertThat(input.getValue().toByteArray()).isEqualTo(TOKEN_3.toByteArray());
});
fakeActionReader
.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID))
.addActionProperties(CONTENT_ID_2, createOpaqueActionData(CONTENT_ID_2));
fakeNetworkClient
.addResponse(createHttpResponse(/* responseCode= */ 200, RESPONSE_1))
.addResponse(createHttpResponse(/* responseCode= */ 200, RESPONSE_2));
requestManager.triggerUploadActions(actionSet, TOKEN_1, consumer);
assertThat(consumer.isCalled()).isTrue();
}
@Test
public void testTriggerUploadActions_batchFirstFailure() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_SIZE_PER_REQUEST, 20)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_ACTIONS_PER_REQUEST, 2)
.build();
requestManager = createRequestManager(configuration);
Set<StreamUploadableAction> actionSet =
setOf(
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build(),
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID_2).build());
consumer =
new RequiredConsumer<>(
input -> {
fakeThreadUtils.checkNotMainThread();
assertThat(input.isSuccessful()).isFalse();
});
fakeActionReader
.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID))
.addActionProperties(CONTENT_ID_2, createOpaqueActionData(CONTENT_ID_2));
fakeNetworkClient.addResponse(createHttpResponse(/* responseCode= */ 500, RESPONSE_1));
requestManager.triggerUploadActions(actionSet, TOKEN_1, consumer);
assertThat(consumer.isCalled()).isTrue();
}
@Test
public void testTriggerUploadActions_batchFirstSuccessSecondFailure() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_SIZE_PER_REQUEST, 20)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_ACTIONS_PER_REQUEST, 2)
.build();
requestManager = createRequestManager(configuration);
Set<StreamUploadableAction> actionSet =
setOf(
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build(),
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID_2).build());
consumer =
new RequiredConsumer<>(
input -> {
fakeThreadUtils.checkNotMainThread();
assertThat(input.isSuccessful()).isTrue();
assertThat(input.getValue().toByteArray()).isEqualTo(TOKEN_2.toByteArray());
});
fakeActionReader
.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID))
.addActionProperties(CONTENT_ID_2, createOpaqueActionData(CONTENT_ID_2));
fakeNetworkClient
.addResponse(createHttpResponse(/* responseCode= */ 200, RESPONSE_1))
.addResponse(createHttpResponse(/* responseCode= */ 500, RESPONSE_2));
requestManager.triggerUploadActions(actionSet, TOKEN_1, consumer);
assertThat(consumer.isCalled()).isTrue();
}
@Test
public void testTriggerUploadActions_batchFirstReachesMax() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_SIZE_PER_REQUEST, 20)
.put(ConfigKey.FEED_ACTION_SERVER_MAX_ACTIONS_PER_REQUEST, 1)
.build();
requestManager = createRequestManager(configuration);
Set<StreamUploadableAction> actionSet =
setOf(
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build(),
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID_2).build());
consumer =
new RequiredConsumer<>(
input -> {
fakeThreadUtils.checkNotMainThread();
assertThat(input.isSuccessful()).isTrue();
assertThat(input.getValue().toByteArray()).isEqualTo(TOKEN_2.toByteArray());
});
fakeActionReader
.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID))
.addActionProperties(CONTENT_ID_2, createOpaqueActionData(CONTENT_ID_2));
fakeNetworkClient.addResponse(createHttpResponse(/* responseCode= */ 200, RESPONSE_1));
requestManager.triggerUploadActions(actionSet, TOKEN_1, consumer);
assertThat(consumer.isCalled()).isTrue();
}
@Test
public void testTriggerUploadActions_getMethod() throws Exception {
Configuration configuration =
new Configuration.Builder()
.put(ConfigKey.FEED_ACTION_SERVER_METHOD, HttpMethod.GET)
.put(ConfigKey.FEED_ACTION_TTL_SECONDS, 1000L)
.put(ConfigKey.FEED_ACTION_MAX_UPLOAD_ATTEMPTS, 2)
.build();
requestManager = createRequestManager(configuration);
StreamUploadableAction action =
StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build();
Set<StreamUploadableAction> actionSet = setOf(action);
fakeActionReader.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID));
fakeNetworkClient.addResponse(
createHttpResponse(/* responseCode= */ 200, Response.getDefaultInstance()));
requestManager.triggerUploadActions(actionSet, ConsistencyToken.getDefaultInstance(), consumer);
HttpRequest httpRequest = fakeNetworkClient.getLatestRequest();
assertHttpRequestFormattedCorrectly(httpRequest);
ActionRequest request = getActionRequestFromHttpRequest(httpRequest);
UploadableActionsRequestBuilder builder =
new UploadableActionsRequestBuilder(fakeActionReader, fakeProtocolAdapter);
ActionRequest expectedRequest =
builder
.setConsistencyToken(ConsistencyToken.getDefaultInstance())
.setActions(actionSet)
.build();
assertThat(request.toByteArray()).isEqualTo(expectedRequest.toByteArray());
assertThat(consumer.isCalled()).isTrue();
assertThat(fakeStore.getContentById(CONTENT_ID))
.contains(
StreamUploadableAction.newBuilder()
.setFeatureContentId(CONTENT_ID)
.setUploadAttempts(1)
.build());
}
@Test
public void testTriggerUploadActions_defaultMethod() throws Exception {
Set<StreamUploadableAction> actionSet =
setOf(StreamUploadableAction.newBuilder().setFeatureContentId(CONTENT_ID).build());
fakeActionReader.addActionProperties(CONTENT_ID, createOpaqueActionData(CONTENT_ID));
fakeNetworkClient.addResponse(
createHttpResponse(/* responseCode= */ 200, Response.getDefaultInstance()));
requestManager.triggerUploadActions(actionSet, ConsistencyToken.getDefaultInstance(), consumer);
HttpRequest httpRequest = fakeNetworkClient.getLatestRequest();
ActionRequest request = getActionRequestFromHttpRequestBody(httpRequest);
UploadableActionsRequestBuilder builder =
new UploadableActionsRequestBuilder(fakeActionReader, fakeProtocolAdapter);
ActionRequest expectedRequest =
builder
.setConsistencyToken(ConsistencyToken.getDefaultInstance())
.setActions(actionSet)
.build();
assertThat(request.toByteArray()).isEqualTo(expectedRequest.toByteArray());
assertThat(consumer.isCalled()).isTrue();
}
private static void assertHttpRequestFormattedCorrectly(HttpRequest httpRequest) {
assertThat(httpRequest.getBody()).hasLength(0);
assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET);
assertThat(httpRequest.getUri().getQueryParameter("fmt")).isEqualTo("bin");
assertThat(httpRequest.getUri().getQueryParameter(RequestHelper.MOTHERSHIP_PARAM_PAYLOAD))
.isNotNull();
}
private static HttpResponse createHttpResponse(int responseCode, Response response)
throws IOException {
byte[] rawResponse = response.toByteArray();
ByteBuffer buffer = ByteBuffer.allocate(rawResponse.length + (Integer.SIZE / 8));
CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(buffer);
codedOutputStream.writeUInt32NoTag(rawResponse.length);
codedOutputStream.writeRawBytes(rawResponse);
codedOutputStream.flush();
return new HttpResponse(responseCode, buffer.array());
}
private ActionRequest getActionRequestFromHttpRequest(HttpRequest httpRequest) throws Exception {
return ActionRequest.parseFrom(
Base64.decode(
httpRequest.getUri().getQueryParameter(RequestHelper.MOTHERSHIP_PARAM_PAYLOAD),
Base64.URL_SAFE),
registry);
}
private ActionRequest getActionRequestFromHttpRequestBody(HttpRequest httpRequest)
throws Exception {
return ActionRequest.parseFrom(httpRequest.getBody(), registry);
}
private FeedActionUploadRequestManager createRequestManager(Configuration configuration) {
return new FeedActionUploadRequestManager(
configuration,
fakeNetworkClient,
fakeProtocolAdapter,
new FeedExtensionRegistry(ArrayList::new),
fakeTaskQueue,
fakeThreadUtils,
fakeActionReader,
fakeStore,
fakeClock);
}
private static OpaqueActionData createOpaqueActionData(String contentId) {
return OpaqueActionData.newBuilder()
.setExtension(
OpaqueActionDataForTest.opaqueActionDataForTestExtension,
OpaqueActionDataForTest.newBuilder().setId(contentId).build())
.build();
}
private static <T> Set<T> setOf(T... items) {
Set<T> result = new HashSet<>();
Collections.addAll(result, items);
return result;
}
}