// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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.api.ads.adwords.extension.ratelimiter;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Mockito;
/**
* Test case for the {@link ApiRateLimiter} class.
*/
@RunWith(JUnit4.class)
public class ApiRateLimiterTest {
private static final Long TEST_CID = 1L;
private static final Object DUMMY_OBJECT = new Object();
private static final Object[] EMPTY_ARGS = new Object[0];
private static final int RETRY_AFTER_SECONDS = 1;
private static final int LONG_RETRY_AFTER_SECONDS = 5;
// Mock the SOAP toolkit agnostic and version agnostic errors.
private abstract static class ApiError {}
private static class RateExceededError extends ApiError {
private String rateScope;
private Integer retryAfterSeconds;
public RateExceededError(String rateScope, Integer retryAfterSeconds) {
this.rateScope = rateScope;
this.retryAfterSeconds = retryAfterSeconds;
}
@SuppressWarnings("unused")
public String getRateScope() {
return rateScope;
}
@SuppressWarnings("unused")
public Integer getRetryAfterSeconds() {
return retryAfterSeconds;
}
}
private static class ApiException extends Exception {
private ApiError[] errors;
public ApiException(ApiError[] errors) {
this.errors = errors;
}
@SuppressWarnings("unused")
public ApiError[] getErrors() {
return errors;
}
}
private static final RateExceededError rateExceededError =
new RateExceededError("DEVELOPER", RETRY_AFTER_SECONDS);
private static final ApiException rateExceededException =
new ApiException(new ApiError[] {rateExceededError});
// RateExceededError with long wait time
private static final RateExceededError rateExceededErrorLong =
new RateExceededError("DEVELOPER", LONG_RETRY_AFTER_SECONDS);
private static final ApiException rateExceededExceptionLong =
new ApiException(new ApiError[] {rateExceededErrorLong});
private static final ApiException otherApiException = new ApiException(new ApiError[] {});
private static final RemoteException remoteException = new RemoteException("message");
// Overwrite default configuration.
private static final int MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR = 3;
private static final int MAX_WAIT_TIME_ON_RATE_EXCEEDED_ERROR = 5;
static {
System.setProperty(
ApiServicesRetryStrategy.MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_PROPERTY,
String.valueOf(MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR));
System.setProperty(
ApiServicesRetryStrategy.MAX_WAIT_TIME_ON_RATE_EXCEEDED_ERROR_PROPERTY,
String.valueOf(MAX_WAIT_TIME_ON_RATE_EXCEEDED_ERROR));
}
// The service that mocks AdWords API services.
private static interface MockService {
Object invoke() throws ApiException, RemoteException;
}
@Mock
private final MockService mockService = Mockito.mock(MockService.class);
private Method method;
private ApiRateLimiter rateLimiter;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() throws Exception {
method = mockService.getClass().getDeclaredMethod("invoke");
rateLimiter = new ApiRateLimiter(ApiServicesRetryStrategy.newInstance());
}
private static void assertExceptionType(Throwable e, Class<?> type) {
assertTrue("Unexpected exception type!", type.isInstance(e));
}
private static void assertExceptionCause(Throwable e, Class<?> type) {
assertTrue("Unexpected exeption cause!", type.isInstance(e.getCause()));
}
// Test that AdWords API call succeeds.
@Test
public void testPass() throws Throwable {
when(mockService.invoke()).thenReturn(DUMMY_OBJECT);
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
}
// Test that AdWords API call failed with RateExceededError first, but succeeds on retry.
@Test
public void testPassAfterOneRetry() throws Throwable {
when(mockService.invoke()).thenThrow(rateExceededException).thenReturn(DUMMY_OBJECT);
long startTime = System.currentTimeMillis();
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
long minWaitMillis =
SECONDS.toMillis(RETRY_AFTER_SECONDS * ApiServicesRetryStrategy.MIN_WAIT_TIME_MULTIPLIER);
assertTrue("Unexpected execution duration!", duration > minWaitMillis);
}
// Test that AdWords API call failed with RateExceededError with all retries.
@Test
public void testFailWithRateExceededError() throws Throwable {
when(mockService.invoke()).thenThrow(rateExceededException);
long startTime = System.currentTimeMillis();
try {
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
} catch (Throwable e) {
assertExceptionType(e, ApiException.class);
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// The last attempt failure will skip wait and propagate error immediately.
int waitedAttempts = MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR - 1;
long minWaitMillis =
SECONDS.toMillis(
RETRY_AFTER_SECONDS
* ApiServicesRetryStrategy.MIN_WAIT_TIME_MULTIPLIER
* waitedAttempts);
assertTrue("Unexpected execution duration!", duration > minWaitMillis);
}
// Test that AdWords API call failed with RateExceededError with all retries.
@Test
public void testMaxWaitTimeWithRateExceededError() throws Throwable {
when(mockService.invoke()).thenThrow(rateExceededExceptionLong);
try {
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
} catch (Throwable e) {
assertExceptionType(e, RateLimiterException.class);
assertExceptionCause(e, ApiException.class);
}
}
// Test that AdWords API call failed with other ApiException.
@Test
public void testFailWithOtherApiException() throws Throwable {
when(mockService.invoke()).thenThrow(otherApiException);
thrown.expect(ApiException.class);
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
}
// Test that AdWords API call failed with RemoteException.
@Test
public void testFailWithRemoteException() throws Throwable {
when(mockService.invoke()).thenThrow(remoteException);
thrown.expect(RemoteException.class);
rateLimiter.run(TEST_CID, mockService, method, EMPTY_ARGS);
}
}