/*
* Copyright (c) 2016 Uber Technologies, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.uber.sdk.android.rides.internal;
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.http.Fault;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.squareup.moshi.Moshi;
import com.uber.sdk.android.rides.RideParameters;
import com.uber.sdk.android.rides.RideRequestButtonCallback;
import com.uber.sdk.rides.client.error.ApiError;
import com.uber.sdk.rides.client.internal.BigDecimalAdapter;
import com.uber.sdk.rides.client.model.PriceEstimate;
import com.uber.sdk.rides.client.model.PriceEstimatesResponse;
import com.uber.sdk.rides.client.model.TimeEstimate;
import com.uber.sdk.rides.client.model.TimeEstimatesResponse;
import com.uber.sdk.rides.client.services.RidesService;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.moshi.MoshiConverterFactory;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.lang.Double.valueOf;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(MockitoJUnitRunner.class)
public class RideRequestButtonControllerTest {
private static final String TIME_ESTIMATES_API = "/v1.2/estimates/time";
private static final String PRICE_ESTIMATES_API = "/v1.2/estimates/price";
private static WireMockConfiguration WIRE_MOCK_CONFIG = wireMockConfig()
.notifier(new ConsoleNotifier(true))
.dynamicPort();
@Rule
public WireMockRule wireMockRule = new WireMockRule(WIRE_MOCK_CONFIG);
@Rule
public ExpectedException expectedException = ExpectedException.none();
private static final String PRODUCT_ID = "a1111c8c-c720-46c3-8534-2fcdd730040d";
private static final float DROP_OFF_LATITUDE = 1.2f;
private static final float DROP_OFF_LONGITUDE = 1.3f;
private static final String DROP_OFF_NICKNAME = "drop off";
private static final String DROP_OFF_ADDRESS = "1455 Market St, Fremont.";
private static final float PICKUP_LONGITUDE = 2.3f;
private static final float PICKUP_LATITUDE = 2.1f;
private static final String PICKUP_ADDRESS = "685 Market St, San Francisco";
private static final String PICKUP_NICKNAME = "pick up";
@Mock
RideRequestButtonView view;
@Mock
Call<TimeEstimatesResponse> timeEstimateCall;
@Mock
Call<PriceEstimatesResponse> priceEstimateCall;
@Mock
RideRequestButtonCallback callback;
private RideRequestButtonController controller;
private RideParameters rideParameters;
private RidesService service;
private CountDownLatch countDownLatch;
private OkHttpClient okHttpClient;
@Before
public void setUp() throws Exception {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setDropoffLocation(valueOf(DROP_OFF_LATITUDE),
valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.build();
Moshi moshi = new Moshi.Builder()
.add(new BigDecimalAdapter())
.build();
okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.readTimeout(1, TimeUnit.SECONDS)
.retryOnConnectionFailure(false)
.build();
countDownLatch = new CountDownLatch(2);
service = new Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.callbackExecutor(new Executor() {
@Override
public void execute(@Nonnull Runnable command) {
command.run();
countDownLatch.countDown();
}
})
.client(okHttpClient)
.baseUrl("http://localhost:" + wireMockRule.port())
.build()
.create(RidesService.class);
controller = new RideRequestButtonController(view, service, callback);
}
@After
public void tearDown() throws Exception {
wireMockRule.resetMappings();
}
@Test
public void testLoadInformationWithProductId_whenEstimatesSuccessful() throws Exception {
stubPriceApiSuccessful();
stubTimeApiWithProductIdSuccessful();
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback).onRideInformationLoaded();
verify(callback, never()).onError(Mockito.any(ApiError.class));
verify(callback, never()).onError(Mockito.any(Throwable.class));
ArgumentCaptor<PriceEstimate> priceCaptor = ArgumentCaptor.forClass(PriceEstimate.class);
ArgumentCaptor<TimeEstimate> timeCaptor = ArgumentCaptor.forClass(TimeEstimate.class);
verify(view).showEstimate(timeCaptor.capture(), priceCaptor.capture());
assertThat(priceCaptor.getValue().getEstimate()).isEqualTo("$9-12");
assertThat(timeCaptor.getValue().getEstimate()).isEqualTo(120);
}
@Test
public void testLoadInformationNoProductId_whenEstimatesSuccessful() throws Exception {
stubPriceApiSuccessful();
stubTimeApiSuccessful();
rideParameters = new RideParameters.Builder()
.setDropoffLocation(valueOf(DROP_OFF_LATITUDE),
valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.build();
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback).onRideInformationLoaded();
verify(callback, never()).onError(Mockito.any(ApiError.class));
verify(callback, never()).onError(Mockito.any(Throwable.class));
ArgumentCaptor<PriceEstimate> priceCaptor = ArgumentCaptor.forClass(PriceEstimate.class);
ArgumentCaptor<TimeEstimate> timeCaptor = ArgumentCaptor.forClass(TimeEstimate.class);
verify(view).showEstimate(timeCaptor.capture(), priceCaptor.capture());
assertThat(priceCaptor.getValue().getEstimate()).isEqualTo("$5.75");
assertThat(timeCaptor.getValue().getEstimate()).isEqualTo(100);
}
@Test
public void testLoadInformation_whenEstimatesSuccessfulButViewDestroyed() throws Exception {
stubPriceApiSuccessful();
stubTimeApiWithProductIdSuccessful();
controller.destroy();
controller.loadRideInformation(rideParameters);
assertThat(controller.pendingDelegate.view).isNull();
assertThat(controller.pendingDelegate.callback).isNull();
countDownLatch.await(3, TimeUnit.SECONDS);
verifyZeroInteractions(callback);
verifyZeroInteractions(view);
}
@Test
public void testLoadInformationTimeOnly_whenEstimatesSuccessfulButViewDestroyed() throws Exception {
stubPriceApiSuccessful();
stubTimeApiWithProductIdSuccessful();
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.build();
controller.destroy();
controller.loadRideInformation(rideParameters);
assertThat(controller.pendingDelegate.view).isNull();
assertThat(controller.pendingDelegate.callback).isNull();
countDownLatch.await(3, TimeUnit.SECONDS);
verifyZeroInteractions(callback);
verifyZeroInteractions(view);
}
@Test
public void testLoadInformation_whenErrorButViewDestroyed() throws Exception {
stubTimeApi(aResponse().withStatus(500));
stubPriceApi(aResponse().withStatus(500));
controller.destroy();
controller.loadRideInformation(rideParameters);
assertThat(controller.pendingDelegate.view).isNull();
assertThat(controller.pendingDelegate.callback).isNull();
countDownLatch.await(3, TimeUnit.SECONDS);
verifyZeroInteractions(callback);
verifyZeroInteractions(view);
}
@Test
public void testLoadInformationWithProductId_whenTimeOnly() throws Exception {
stubPriceApi(aResponse().withStatus(500));
stubTimeApiWithProductIdSuccessful();
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.build();
controller.loadRideInformation(rideParameters);
countDownLatch.countDown();
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback).onRideInformationLoaded();
verify(callback, never()).onError(Mockito.any(ApiError.class));
verify(callback, never()).onError(Mockito.any(Throwable.class));
ArgumentCaptor<TimeEstimate> timeCaptor = ArgumentCaptor.forClass(TimeEstimate.class);
verify(view).showEstimate(timeCaptor.capture());
assertThat(timeCaptor.getValue().getEstimate()).isEqualTo(120);
}
@Test
public void testLoadInformationNoProductId_whenTimeOnly() throws Exception {
stubPriceApi(aResponse().withStatus(500));
stubTimeApiSuccessful();
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.build();
controller.loadRideInformation(rideParameters);
countDownLatch.countDown();
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback).onRideInformationLoaded();
verify(callback, never()).onError(Mockito.any(ApiError.class));
verify(callback, never()).onError(Mockito.any(Throwable.class));
ArgumentCaptor<TimeEstimate> timeCaptor = ArgumentCaptor.forClass(TimeEstimate.class);
verify(view).showEstimate(timeCaptor.capture());
assertThat(timeCaptor.getValue().getEstimate()).isEqualTo(120);
}
@Test
public void testLoadInformation_whenPriceReturnsApiError() throws Exception {
stubPriceApi(aResponse().withStatus(500));
stubTimeApiWithProductIdSuccessful();
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(500);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenTimeReturnsApiError() throws Exception {
stubPriceApiSuccessful();
stubTimeApi(aResponse().withStatus(500));
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(500);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenNoMatchingProductTimeEstimates() throws Exception {
stubPriceApiSuccessful();
stubTimeApi(aResponse().withBodyFile("times_estimate_no_uberx.json"));
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(404);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenNoMatchingProductPriceEstimates() throws Exception {
stubTimeApiSuccessful();
stubPriceApi(aResponse().withBodyFile("prices_estimate_no_uberx.json"));
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(404);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenPriceEstimatesEmpty() throws Exception {
stubTimeApiSuccessful();
stubPriceApi(aResponse().withBody("{}"));
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(404);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenTimeEstimatesEmpty() throws Exception {
stubTimeApi(aResponse().withBody("{}"));
stubPriceApiSuccessful();
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<ApiError> errorCaptor = ArgumentCaptor.forClass(ApiError.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue().getClientErrors().get(0).getStatus()).isEqualTo(404);
verify(callback, never()).onError(any(Throwable.class));
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenPriceFails() throws Exception {
stubTimeApi(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE));
stubPriceApiSuccessful();
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<Throwable> errorCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue()).isNotNull();
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_whenTimeFails() throws Exception {
stubTimeApiSuccessful();
stubPriceApi(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE));
controller.loadRideInformation(rideParameters);
countDownLatch.await(3, TimeUnit.SECONDS);
verify(callback, never()).onRideInformationLoaded();
ArgumentCaptor<Throwable> errorCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(callback).onError(errorCaptor.capture());
assertThat(errorCaptor.getValue()).isNotNull();
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
verify(view).showDefaultView();
}
@Test
public void testLoadInformation_wheNoPickup() {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setDropoffLocation(valueOf(DROP_OFF_LATITUDE),
valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.build();
controller.loadRideInformation(rideParameters);
verify(view).showDefaultView();
verify(view, never()).showEstimate(any(TimeEstimate.class));
verify(view, never()).showEstimate(any(TimeEstimate.class), any(PriceEstimate.class));
}
@Test
public void testLoadInformation_wheNoPickupLongitude() {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), null, PICKUP_NICKNAME, PICKUP_ADDRESS)
.setDropoffLocation(valueOf(DROP_OFF_LATITUDE),
valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.build();
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("Pickup point latitude is set in " +
"RideParameters but not the longitude.");
controller.loadRideInformation(rideParameters);
}
@Test
public void testLoadInformation_wheNoPickupLatitude() {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(null, valueOf(PICKUP_LONGITUDE), PICKUP_NICKNAME, PICKUP_ADDRESS)
.setDropoffLocation(null, valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.build();
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("Pickup point longitude is set in " +
"RideParameters but not the latitude.");
controller.loadRideInformation(rideParameters);
}
@Test
public void testLoadInformation_wheNoDropOffLatitude() {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.setDropoffLocation(null, valueOf(DROP_OFF_LONGITUDE), DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.build();
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("Dropoff point longitude is set in RideParameters but not the latitude.");
controller.loadRideInformation(rideParameters);
}
@Test
public void testLoadInformation_wheNoDropOffLongitude() {
rideParameters = new RideParameters.Builder()
.setProductId(PRODUCT_ID)
.setPickupLocation(valueOf(PICKUP_LATITUDE), valueOf(PICKUP_LONGITUDE),
PICKUP_NICKNAME, PICKUP_ADDRESS)
.setDropoffLocation(valueOf(DROP_OFF_LATITUDE), null, DROP_OFF_NICKNAME, DROP_OFF_ADDRESS)
.build();
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("Dropoff point latitude is set in RideParameters but not the longitude.");
controller.loadRideInformation(rideParameters);
}
private static void stubPriceApi(ResponseDefinitionBuilder responseBuilder) {
stubFor(get(urlPathEqualTo(PRICE_ESTIMATES_API))
.withQueryParam("start_latitude", equalTo(String.valueOf(PICKUP_LATITUDE)))
.withQueryParam("start_longitude", equalTo(String.valueOf(PICKUP_LONGITUDE)))
.withQueryParam("end_latitude", equalTo(String.valueOf(DROP_OFF_LATITUDE)))
.withQueryParam("end_longitude", equalTo(String.valueOf(DROP_OFF_LONGITUDE)))
.willReturn(responseBuilder));
}
private static void stubPriceApiSuccessful() {
stubPriceApi(aResponse().withBodyFile("prices_estimate.json"));
}
private static void stubTimeApiWithProductIdSuccessful() {
stubFor(get(urlPathMatching(TIME_ESTIMATES_API))
.withQueryParam("start_latitude", equalTo(String.valueOf(PICKUP_LATITUDE)))
.withQueryParam("start_longitude", equalTo(String.valueOf(PICKUP_LONGITUDE)))
.withQueryParam("product_id", equalTo(PRODUCT_ID))
.willReturn(aResponse().withBodyFile("time_estimate_uberx.json")));
}
private static void stubTimeApiSuccessful() {
stubTimeApi(aResponse().withBodyFile("times_estimate.json"));
}
private static void stubTimeApi(ResponseDefinitionBuilder responseBuilder) {
stubFor(get(urlPathMatching(TIME_ESTIMATES_API))
.withQueryParam("start_latitude", equalTo(String.valueOf(PICKUP_LATITUDE)))
.withQueryParam("start_longitude", equalTo(String.valueOf(PICKUP_LONGITUDE)))
.willReturn(responseBuilder));
}
}