package org.solovyev.android.checkout; import com.android.vending.billing.IInAppBillingService; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import android.os.Bundle; import android.os.RemoteException; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; import static java.lang.System.currentTimeMillis; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyObject; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.solovyev.android.checkout.RequestTestBase.newBundle; import static org.solovyev.android.checkout.ResponseCodes.OK; @SuppressWarnings("unchecked") @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class BillingTest { @Nonnull private Billing mBilling; @Nonnull private Random mRandom; @Nonnull static Bundle newPurchasesBundle(long id, boolean withContinuationToken) throws JSONException { final Bundle bundle = newBundle(OK); final ArrayList<String> list = new ArrayList<String>(); list.add(PurchaseTest.newJson(id, Purchase.State.PURCHASED)); bundle.putStringArrayList(Purchases.BUNDLE_DATA_LIST, list); if (withContinuationToken) { bundle.putString(Purchases.BUNDLE_CONTINUATION_TOKEN, String.valueOf(id + 1)); } return bundle; } @Before public void setUp() throws Exception { mBilling = Tests.newSynchronousBilling(); mRandom = new Random(currentTimeMillis()); } @Test public void testShouldNotifyErrorIfCantConnect() throws Exception { final Billing.ServiceConnector connector = mock(Billing.ServiceConnector.class); when(connector.connect()).thenReturn(false); mBilling.setConnector(connector); final RequestListener<Object> l = mock(RequestListener.class); mBilling.getRequests().isBillingSupported("p", l); verify(l, times(1)).onError(eq(ResponseCodes.SERVICE_NOT_CONNECTED), any(BillingException.class)); verify(l, times(0)).onSuccess(any()); } @Test public void testShouldNotifyErrorIfConnectorReturnedNull() throws Exception { final Billing.ServiceConnector connector = mock(Billing.ServiceConnector.class); when(connector.connect()).then(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { mBilling.setService(null, true); return true; } }); mBilling.setConnector(connector); final RequestListener<Object> l = mock(RequestListener.class); mBilling.getRequests().isBillingSupported("p", l); verify(l, times(1)).onError(eq(ResponseCodes.SERVICE_NOT_CONNECTED), any(BillingException.class)); verify(l, times(0)).onSuccess(any()); } @Test public void testShouldExecuteRequestIfConnected() throws Exception { final Billing.ServiceConnector connector = mock(Billing.ServiceConnector.class); final IInAppBillingService service = mock(IInAppBillingService.class); when(service.isBillingSupported(anyInt(), anyString(), anyString())).thenReturn(OK); when(connector.connect()).then(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { mBilling.setService(service, true); return true; } }); mBilling.setConnector(connector); final RequestListener<Object> l = mock(RequestListener.class); mBilling.getRequests().isBillingSupported("p", l); verify(l, times(0)).onError(anyInt(), any(BillingException.class)); verify(l, times(1)).onSuccess(any()); } @Test public void testStates() throws Exception { final Billing.ServiceConnector connector = mock(Billing.ServiceConnector.class); when(connector.connect()).thenReturn(true); mBilling.setConnector(connector); mBilling.connect(); assertEquals(Billing.State.CONNECTING, mBilling.getState()); mBilling.setService(mock(IInAppBillingService.class), true); assertEquals(Billing.State.CONNECTED, mBilling.getState()); mBilling.disconnect(); assertEquals(Billing.State.DISCONNECTING, mBilling.getState()); mBilling.setService(null, false); assertEquals(Billing.State.DISCONNECTED, mBilling.getState()); } @Test public void testShouldDisconnectOnlyIfNotInInitialState() throws Exception { mBilling.setState(Billing.State.INITIAL); mBilling.setService(null, false); assertEquals(Billing.State.INITIAL, mBilling.getState()); mBilling.setState(Billing.State.CONNECTING); mBilling.setState(Billing.State.CONNECTED); mBilling.setState(Billing.State.DISCONNECTING); mBilling.setService(null, false); assertEquals(Billing.State.DISCONNECTED, mBilling.getState()); mBilling.setState(Billing.State.CONNECTING); mBilling.setState(Billing.State.CONNECTED); mBilling.setService(null, false); assertEquals(Billing.State.DISCONNECTED, mBilling.getState()); } @Test public void testShouldConnectOnlyIfConnecting() throws Exception { mBilling.setState(Billing.State.CONNECTING); mBilling.setState(Billing.State.FAILED); mBilling.setService(mock(IInAppBillingService.class), true); assertEquals(Billing.State.FAILED, mBilling.getState()); mBilling.setState(Billing.State.CONNECTING); mBilling.setService(mock(IInAppBillingService.class), true); assertEquals(Billing.State.CONNECTED, mBilling.getState()); } @Test public void testShouldJumpToDisconnectedStateIfWasConnecting() throws Exception { mBilling.setState(Billing.State.CONNECTING); mBilling.setService(null, false); assertEquals(Billing.State.FAILED, mBilling.getState()); } @Test public void testShouldDisconnectServiceIfBillingIsInactive() throws Exception { final Billing.ServiceConnector connector = mock(Billing.ServiceConnector.class); mBilling.setConnector(connector); mBilling.setState(Billing.State.CONNECTING); mBilling.setState(Billing.State.DISCONNECTED); mBilling.setService(mock(IInAppBillingService.class), true); assertEquals(Billing.State.DISCONNECTED, mBilling.getState()); verify(connector, times(1)).disconnect(); } @Test public void testShouldGoToDisconnectedStateFromConnectingIfBillingDies() throws Exception { mBilling.setState(Billing.State.CONNECTING); mBilling.disconnect(); assertEquals(Billing.State.DISCONNECTED, mBilling.getState()); } @Test public void testShouldRunAllRequests() throws Exception { final int REQUESTS = 100; final int SLEEP = 10; final Billing b = Tests.newBilling(false); b.setMainThread(Tests.sameThreadExecutor()); final AsyncServiceConnector c = new AsyncServiceConnector(b); b.setConnector(c); final CountDownLatch latch = new CountDownLatch(REQUESTS); final RequestListener l = new CountDownListener(latch); for (int i = 0; i < REQUESTS; i++) { if (i % 10 == 0) { if (mRandom.nextBoolean()) { b.connect(); } else { // connector is called directly in order to avoid cancelling the pending // requests c.disconnect(); } } b.runWhenConnected(new SleepingRequest(mRandom.nextInt(SLEEP)), l, null); } b.connect(); assertTrue(latch.await(SLEEP * REQUESTS, TimeUnit.MILLISECONDS)); } @Test public void testShouldCancelRequests() throws Exception { final int REQUESTS = 10; final Billing b = Tests.newBilling(false); final CountDownLatch latch = new CountDownLatch(REQUESTS / 2); final RequestListener l = new CountDownListener(latch); final List<Integer> requestIds = new ArrayList<Integer>(); for (int i = 0; i < REQUESTS; i++) { requestIds.add(b.runWhenConnected(new SleepingRequest(100), l, null)); } Thread.sleep(100 * (REQUESTS / 2 - 1)); for (int i = REQUESTS / 2; i < REQUESTS; i++) { b.cancel(requestIds.get(i)); } assertTrue(latch.await(1, SECONDS)); } @Test public void testIsPurchasedShouldCollectAllThePurchases() throws Exception { checkIsPurchased("0", true); checkIsPurchased("1", true); checkIsPurchased("2", true); checkIsPurchased("3", true); checkIsPurchased("4", true); checkIsPurchased("-1", false); checkIsPurchased("5", false); } @Test public void testShouldReturnAllPurchases() throws Exception { final Billing billing = prepareMultiPurchasesBilling(); final CountDownLatch latch = new CountDownLatch(1); final CountDownListener l = new CountDownListener(latch); billing.getRequests().getAllPurchases(ProductTypes.IN_APP, l); assertTrue(latch.await(1, SECONDS)); verify(l.listener).onSuccess(argThat(new BaseMatcher<Purchases>() { @Override public boolean matches(Object item) { if (!(item instanceof Purchases)) { return false; } final Purchases purchases = (Purchases) item; for (Integer id : asList(0, 1, 2, 3, 4)) { if (!purchases.hasPurchaseInState(String.valueOf(id), Purchase.State.PURCHASED)) { return false; } } return true; } @Override public void describeTo(Description description) { } })); } @Test public void testShouldCancelIsPurchasedListener() throws Exception { final Billing billing = Tests.newBilling(true); final CountDownLatch requestWaiter = new CountDownLatch(1); final CountDownLatch cancelWaiter = new CountDownLatch(1); final IInAppBillingService service = mock(IInAppBillingService.class); when(service.getPurchases(anyInt(), anyString(), anyString(), isNull(String.class))).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { requestWaiter.countDown(); return newPurchasesBundle(0, true); } }); when(service.getPurchases(anyInt(), anyString(), anyString(), eq("1"))).thenAnswer(new Answer<Bundle>() { @Override public Bundle answer(InvocationOnMock invocation) throws Throwable { cancelWaiter.await(1, SECONDS); return newPurchasesBundle(1, false); } }); Tests.setService(billing, service); final RequestListener l = mock(RequestListener.class); final BillingRequests requests = billing.getRequests(); requests.isPurchased(ProductTypes.IN_APP, "1", l); requestWaiter.await(1, SECONDS); requests.cancelAll(); cancelWaiter.countDown(); verify(l, never()).onSuccess(anyObject()); verify(l, never()).onError(anyInt(), any(Exception.class)); } private void checkIsPurchased(@Nonnull String id, boolean purchased) throws RemoteException, JSONException, InterruptedException { final Billing billing = prepareMultiPurchasesBilling(); final CountDownLatch latch = new CountDownLatch(1); final CountDownListener l = new CountDownListener(latch); billing.getRequests().isPurchased(ProductTypes.IN_APP, id, l); assertTrue(latch.await(1, SECONDS)); verify(l.listener).onSuccess(eq(purchased)); verify(l.listener, never()).onSuccess(eq(!purchased)); } @Nonnull private Billing prepareMultiPurchasesBilling() throws RemoteException, JSONException { final Billing billing = Tests.newBilling(true); prepareMultiPurchasesService(billing); return billing; } private void prepareMultiPurchasesService(@Nonnull Billing billing) throws RemoteException, JSONException { final IInAppBillingService service = mock(IInAppBillingService.class); when(service.getPurchases(anyInt(), anyString(), anyString(), isNull(String.class))).thenReturn(newPurchasesBundle(0, true)); when(service.getPurchases(anyInt(), anyString(), anyString(), eq("1"))).thenReturn(newPurchasesBundle(1, true)); when(service.getPurchases(anyInt(), anyString(), anyString(), eq("2"))).thenReturn(newPurchasesBundle(2, true)); when(service.getPurchases(anyInt(), anyString(), anyString(), eq("3"))).thenReturn(newPurchasesBundle(3, true)); when(service.getPurchases(anyInt(), anyString(), anyString(), eq("4"))).thenReturn(newPurchasesBundle(4, false)); Tests.setService(billing, service); } @Test public void testShouldAutoDisconnect() throws Exception { final Billing billing = Tests.newBilling(true, true); assertTrue(billing.getState() == Billing.State.INITIAL); billing.onCheckoutStarted(); assertTrue(billing.getState() == Billing.State.CONNECTED); billing.onCheckoutStarted(); assertTrue(billing.getState() == Billing.State.CONNECTED); billing.onCheckoutStopped(); assertTrue(billing.getState() == Billing.State.CONNECTED); billing.onCheckoutStopped(); assertTrue(billing.getState() == Billing.State.DISCONNECTED); } private static class CountDownListener<R> implements RequestListener<R> { private final CountDownLatch latch; private final RequestListener<R> listener; public CountDownListener(CountDownLatch latch) { this.latch = latch; this.listener = mock(RequestListener.class); } @Override public void onSuccess(@Nonnull R result) { listener.onSuccess(result); onEnd(); } private void onEnd() { latch.countDown(); } @Override public void onError(int response, @Nonnull Exception e) { listener.onError(response, e); onEnd(); } } private final class SleepingRequest extends Request { private final long sleep; private SleepingRequest(long sleep) { super(RequestType.BILLING_SUPPORTED); this.sleep = sleep; } @Override void start(@Nonnull IInAppBillingService service, @Nonnull String packageName) throws RemoteException, RequestException { try { Thread.sleep(sleep); onSuccess(new Object()); } catch (InterruptedException e) { throw new RequestException(e); } } @Nullable @Override String getCacheKey() { return null; } } }