package com.github.jberkel.pay.me; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import com.android.vending.billing.IInAppBillingService; import com.github.jberkel.pay.me.listener.OnConsumeFinishedListener; import com.github.jberkel.pay.me.listener.OnConsumeMultiFinishedListener; import com.github.jberkel.pay.me.listener.OnIabPurchaseFinishedListener; import com.github.jberkel.pay.me.listener.OnIabSetupFinishedListener; import com.github.jberkel.pay.me.listener.QueryInventoryFinishedListener; import com.github.jberkel.pay.me.model.Inventory; import com.github.jberkel.pay.me.model.Purchase; import com.github.jberkel.pay.me.validator.SignatureValidator; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.res.builder.RobolectricPackageManager; import org.robolectric.shadows.ShadowLog; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static com.github.jberkel.pay.me.IabConsts.*; import static com.github.jberkel.pay.me.Response.*; import static com.github.jberkel.pay.me.model.ItemType.INAPP; import static com.github.jberkel.pay.me.model.ItemType.SUBS; import static org.fest.assertions.api.Assertions.assertThat; import static org.fest.assertions.api.Assertions.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.*; @RunWith(RobolectricTestRunner.class) public class IabHelperTest { private static final int TEST_REQUEST_CODE = 42; @Mock private IInAppBillingService service; @Mock private OnIabSetupFinishedListener setupListener; @Mock private OnIabPurchaseFinishedListener purchaseFinishedListener; @Mock private SignatureValidator signatureValidator; private IabHelper helper; @Before public void before() { MockitoAnnotations.initMocks(this); when(signatureValidator.validate(anyString(), anyString())).thenReturn(true); helper = new IabHelper(Robolectric.application, signatureValidator) { @Override protected IInAppBillingService getInAppBillingService(IBinder binder) { return service; } }; helper.enableDebugLogging(true, getClass().getSimpleName()); ShadowLog.stream = System.out; } @After public void after() { /* verify(service); */ } @Test public void shouldCreateHelper() throws Exception { IabHelper helper = new IabHelper(Robolectric.application, PUBLIC_KEY); assertThat(helper.isDisposed()).isFalse(); } @Test public void shouldCreateHelperWithValidator() throws Exception { SignatureValidator validator = mock(SignatureValidator.class); new IabHelper(Robolectric.application, validator); } @Test(expected = IllegalArgumentException.class) public void shouldRequireValidator() throws Exception { new IabHelper(Robolectric.application, (SignatureValidator)null); } @Test(expected = IllegalArgumentException.class) public void shouldCheckPublicKeyEagerly() throws Exception { new IabHelper(Robolectric.application, "INVALIDKEY"); } @Test public void shouldStartSetup_SuccessCase() throws Exception { registerServiceWithPackageManager(); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(OK)); } @Test public void shouldBeInSetupStateWhenFinishedListenerIsCalled() throws Exception { registerServiceWithPackageManager(); helper.startSetup(new OnIabSetupFinishedListener() { @Override public void onIabSetupFinished(IabResult result) { assertThat(result.getResponse()).isEqualTo(OK); assertThat(helper.isDisposed()).isFalse(); helper.checkSetupDone("test"); } }); } @Test public void shouldBeInSetupStateWhenFinishedListenerIsCalledNoBillingAvailable() throws Exception { helper.startSetup(new OnIabSetupFinishedListener() { @Override public void onIabSetupFinished(IabResult result) { assertThat(result.getResponse()).isEqualTo(BILLING_UNAVAILABLE); assertThat(helper.isDisposed()).isFalse(); helper.checkSetupDone("test"); } }); } @Test public void shouldStartSetup_BillingServiceDoesNotExist() throws Exception { helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(BILLING_UNAVAILABLE)); } @Test public void shouldStartSetup_ServiceDoesNotSupportBilling() throws Exception { registerServiceWithPackageManager(); when(service.isBillingSupported(eq(API_VERSION), anyString(), anyString())).thenReturn(BILLING_UNAVAILABLE.code); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(BILLING_UNAVAILABLE)); assertThat(helper.subscriptionsSupported()).isFalse(); } @Test public void shouldStartSetup_CheckForSubscriptions_Unavailable() throws Exception { registerServiceWithPackageManager(); when(service.isBillingSupported(eq(API_VERSION), anyString(), eq("inapp"))).thenReturn(OK.code); when(service.isBillingSupported(eq(API_VERSION), anyString(), eq("subs"))).thenReturn(BILLING_UNAVAILABLE.code); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(OK)); assertThat(helper.subscriptionsSupported()).isFalse(); } @Test public void shouldStartSetup_CheckForSubscriptions_Success() throws Exception { registerServiceWithPackageManager(); when(service.isBillingSupported(eq(API_VERSION), anyString(), eq("inapp"))).thenReturn(OK.code); when(service.isBillingSupported(eq(API_VERSION), anyString(), eq("subs"))).thenReturn(OK.code); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(OK)); assertThat(helper.subscriptionsSupported()).isTrue(); } @Test public void shouldStartSetup_ServiceExistsButThrowsException() throws Exception { registerServiceWithPackageManager(); when(service.isBillingSupported(eq(API_VERSION), anyString(), anyString())).thenThrow(new RemoteException()); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION)); assertThat(helper.subscriptionsSupported()).isFalse(); } @Test public void shouldStartSetup_ServiceExistsButDisposed() throws Exception { registerServiceWithPackageManager(); when(service.isBillingSupported(eq(API_VERSION), anyString(), anyString())).thenReturn(OK.code); helper.startSetup(setupListener); verify(setupListener).onIabSetupFinished(new IabResult(OK)); helper.dispose(); assertThat(helper.subscriptionsSupported()).isFalse(); } @Test public void shouldDisposeAfterStartupAndUnbindServiceConnection() throws Exception { shouldStartSetup_SuccessCase(); assertThat(helper.isDisposed()).isFalse(); helper.dispose(); List<ServiceConnection> unboundServiceConnections = Robolectric.shadowOf(Robolectric.application).getUnboundServiceConnections(); assertThat(unboundServiceConnections).hasSize(1); assertThat(helper.isDisposed()).isTrue(); } @Test (expected = IllegalStateException.class) public void shouldRaiseExceptionIfStartingAndObjectIsDisposed() throws Exception { shouldStartSetup_SuccessCase(); helper.dispose(); helper.startSetup(setupListener); } // launchPurchaseFlow @Test(expected = IllegalArgumentException.class) public void shouldRaiseExceptionIfPurchaseFlowLaunchedWithEmptySky() throws Exception { shouldStartSetup_SuccessCase(); helper.launchPurchaseFlow(mock(Activity.class), "", INAPP, 0, purchaseFinishedListener, null); } @Test(expected = IllegalArgumentException.class) public void shouldRaiseExceptionIfPurchaseFlowLaunchedWithNullType() throws Exception { shouldStartSetup_SuccessCase(); helper.launchPurchaseFlow(mock(Activity.class), "sku", null, 0, purchaseFinishedListener, null); } @Test(expected = IllegalStateException.class) public void shouldRaiseExceptionIfPurchaseFlowLaunchedWithoutSetup() throws Exception { helper.launchPurchaseFlow(mock(Activity.class), "sku", INAPP, 0, purchaseFinishedListener, ""); } @Test public void shouldCallListenerWithDisposedReponseIfHelperIsDisposed() throws Exception { shouldStartSetup_SuccessCase(); helper.dispose(); helper.launchPurchaseFlow(mock(Activity.class), "sku", INAPP, 0, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished(new IabResult(Response.IABHELPER_DISPOSED), null); } @Test public void shouldFailPurchaseWhenEmptyResponseIsReturned() throws Exception { shouldStartSetup_SuccessCase(); Bundle empty = new Bundle(); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "inapp", "")).thenReturn(empty); helper.launchPurchaseFlow(mock(Activity.class), "sku", INAPP, 0, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(IABHELPER_SEND_INTENT_FAILED), null); } @Test public void shouldFailPurchaseWhenErrorIsReturned() throws Exception { shouldStartSetup_SuccessCase(); Bundle errorResponse = new Bundle(); errorResponse.putInt(RESPONSE_CODE, ERROR.code); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "inapp", "")).thenReturn(errorResponse); helper.launchPurchaseFlow(mock(Activity.class), "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(ERROR, "Unable to buy item"), null); } @Test public void shouldFailPurchaseWhenSendIntentExceptionIsThrown() throws Exception { shouldStartSetup_SuccessCase(); Activity activity = new Activity() { @Override public void startIntentSenderForResult(IntentSender intent, int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { throw new IntentSender.SendIntentException("Failz"); } }; Bundle response = new Bundle(); response.putParcelable(RESPONSE_BUY_INTENT, PendingIntent.getActivity(Robolectric.application, 0, new Intent(), 0)); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "inapp", "")).thenReturn(response); helper.launchPurchaseFlow(activity, "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(IABHELPER_SEND_INTENT_FAILED), null); } @Test public void shouldFailPurchaseWhenRemoteExceptionIsThrown() throws Exception { shouldStartSetup_SuccessCase(); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "inapp", "")).thenThrow(new RemoteException()); helper.launchPurchaseFlow(null, "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(IABHELPER_REMOTE_EXCEPTION), null); } @Test public void shouldFailPurchaseWhenBillingUnsupported() throws Exception { shouldStartSetup_ServiceDoesNotSupportBilling(); helper.launchPurchaseFlow(null, "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(BILLING_UNAVAILABLE), null); } @Test public void shouldFailPurchaseWhenBillingUnsupported_NoService() throws Exception { shouldStartSetup_BillingServiceDoesNotExist(); helper.launchPurchaseFlow(null, "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(BILLING_UNAVAILABLE), null); } @Test public void shouldFailSubscriptionPurchaseWhenUnsupported() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); helper.launchPurchaseFlow(null, "sku", SUBS, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(purchaseFinishedListener).onIabPurchaseFinished( new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE), null); } @Test public void shouldStartIntentAfterSuccessfulLaunchPurchase() throws Exception { shouldStartSetup_SuccessCase(); Bundle response = new Bundle(); response.putParcelable(RESPONSE_BUY_INTENT, PendingIntent.getActivity(Robolectric.application, 0, new Intent(), 0)); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "inapp", "")).thenReturn(response); Activity activity = mock(Activity.class); helper.launchPurchaseFlow(activity, "sku", INAPP, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(activity).startIntentSenderForResult(any(IntentSender.class), eq(TEST_REQUEST_CODE), any(Intent.class), eq(0), eq(0), eq(0)); } @Test public void shouldStartIntentAfterSuccessfulLaunchPurchaseForSubscription() throws Exception { shouldStartSetup_SuccessCase(); Bundle response = new Bundle(); response.putParcelable(RESPONSE_BUY_INTENT, PendingIntent.getActivity(Robolectric.application, 0, new Intent(), 0)); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "subs", "")).thenReturn(response); Activity activity = mock(Activity.class); helper.launchPurchaseFlow(activity, "sku", SUBS, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(activity).startIntentSenderForResult(any(IntentSender.class), eq(TEST_REQUEST_CODE), any(Intent.class), eq(0), eq(0), eq(0)); } @Test public void shouldLaunchSubscriptionPurchaseFlowWithoutExtraData() throws Exception { shouldStartSetup_SuccessCase(); Bundle response = new Bundle(); response.putParcelable(RESPONSE_BUY_INTENT, PendingIntent.getActivity(Robolectric.application, 0, new Intent(), 0)); when(service.getBuyIntent(API_VERSION, Robolectric.application.getPackageName(), "sku", "subs", "")).thenReturn(response); Activity activity = mock(Activity.class); helper.launchPurchaseFlow(activity, "sku", SUBS, TEST_REQUEST_CODE, purchaseFinishedListener, ""); verify(activity).startIntentSenderForResult(any(IntentSender.class), eq(TEST_REQUEST_CODE), any(Intent.class), eq(0), eq(0), eq(0)); } // handleActivityResult @Test public void handleActivityResultRequestCodeMismatch() throws Exception { assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, 0, null)).isFalse(); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultNullData() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, 0, null)).isTrue(); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithData() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); data.putExtra(RESPONSE_INAPP_PURCHASE_DATA, "{ \"productId\": \"foo\" }"); data.putExtra(RESPONSE_INAPP_SIGNATURE, ""); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished(eq(new IabResult(OK)), any(Purchase.class)); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithoutData() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished(eq(new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature")), any(Purchase.class)); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithInvalidData() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); data.putExtra(RESPONSE_INAPP_PURCHASE_DATA, "this is not json"); data.putExtra(RESPONSE_INAPP_SIGNATURE, ""); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished(new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."), null); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithErrorResponseCode() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, ERROR.code); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished(new IabResult(ERROR, "Problem purchashing item."), null); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithCanceledResultCode() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, ITEM_UNAVAILABLE.code); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_CANCELED, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished(new IabResult(ITEM_UNAVAILABLE), null); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWithUnknownResultCode() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, 23, data)).isFalse(); verify(purchaseFinishedListener).onIabPurchaseFinished(new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE), null); } @Test public void shouldLaunchPurchaseAndStartIntentAndThenHandleActivityResultWhenDisposed() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); data.putExtra(RESPONSE_INAPP_PURCHASE_DATA, "{ \"productId\": \"foo\" }"); data.putExtra(RESPONSE_INAPP_SIGNATURE, ""); helper.dispose(); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isFalse(); } @Test public void shouldHandleInvalidSignature() throws Exception { shouldStartIntentAfterSuccessfulLaunchPurchase(); Intent data = new Intent(); data.putExtra(RESPONSE_CODE, OK.code); String purchaseData = "{ \"productId\": \"foo\" }"; data.putExtra(RESPONSE_INAPP_PURCHASE_DATA, purchaseData); data.putExtra(RESPONSE_INAPP_SIGNATURE, "some signature"); when(signatureValidator.validate(eq(purchaseData), eq("some signature"))).thenReturn(false); assertThat(helper.handleActivityResult(TEST_REQUEST_CODE, Activity.RESULT_OK, data)).isTrue(); verify(purchaseFinishedListener).onIabPurchaseFinished( eq(new IabResult(IABHELPER_VERIFICATION_FAILED)), any(Purchase.class)); } // inventory @Test public void shouldQueryInventoryWithoutSubscriptions() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle("foo"); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); Inventory inventory = helper.queryInventory(false, null ,null); assertThat(inventory.getAllPurchases()).hasSize(1); assertThat(inventory.getAllOwnedSkus()).hasSize(1); assertThat(inventory.getSkuDetails()).isEmpty(); } @Test(expected = IabException.class) public void shouldQueryInventoryWhenDisposedThrowIabException() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); helper.dispose(); Inventory inventory = helper.queryInventory(false, null ,null); } @Test public void shouldQueryInventoryWithoutSubscriptionsButSkuDetails() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle("foo"); Bundle skuDetails = createSkuDetailsResponseBundle("foo"); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), TestHelper.BundleStringArrayListMatcher.bundleWithStringValues(GET_SKU_DETAILS_ITEM_LIST, "foo"))) .thenReturn(skuDetails); Inventory inventory = helper.queryInventory(true, null ,null); assertThat(inventory.getAllPurchases()).hasSize(1); assertThat(inventory.getAllOwnedSkus()).hasSize(1); assertThat(inventory.getSkuDetails()).hasSize(1); } @Test public void shouldQueryInventoryWithoutSubscriptionsButSkuDetailsAndMoreSkus() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle("foo"); Bundle skuDetails = createSkuDetailsResponseBundle("foo"); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), TestHelper.BundleStringArrayListMatcher.bundleWithStringValues(GET_SKU_DETAILS_ITEM_LIST, "foo", "anotherSku"))) .thenReturn(skuDetails); Inventory inventory = helper.queryInventory(true, asArrayList("anotherSku"), null); assertThat(inventory.getAllPurchases()).hasSize(1); assertThat(inventory.getAllOwnedSkus()).hasSize(1); assertThat(inventory.getSkuDetails()).hasSize(1); } @Test(expected = IabException.class) public void shouldQueryInventoryWithoutSubscriptionsButSkuDetailsAndSkuError() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle("foo"); Bundle skuDetails = new Bundle(); skuDetails.putInt(RESPONSE_CODE, ERROR.code); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), any(Bundle.class))) .thenReturn(skuDetails); helper.queryInventory(true, null, null); } @Test(expected = IabException.class) public void shouldQueryInventoryButServiceReturnsInconsistentData() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = new Bundle(); response.putStringArrayList(RESPONSE_INAPP_ITEM_LIST, asArrayList()); // NB empty list here response.putStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST, asArrayList("{}")); response.putStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST, asArrayList("")); response.putString(INAPP_CONTINUATION_TOKEN, ""); Bundle skuDetails = new Bundle(); skuDetails.putInt(RESPONSE_CODE, OK.code); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), any(Bundle.class))) .thenReturn(skuDetails); helper.queryInventory(true, null, null); } @Test public void shouldQueryInventoryWithEmptySkuDetails() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle(); Bundle skuDetails = new Bundle(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), any(Bundle.class))) .thenReturn(skuDetails); Inventory inv = helper.queryInventory(true, null, null); assertThat(inv.getAllOwnedSkus().isEmpty()); } @Test public void shouldQueryInventoryEmptySkuDetails() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = createInventoryResponseBundle(); Bundle skuDetails = new Bundle(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), any(Bundle.class))) .thenReturn(skuDetails); Inventory inv = helper.queryInventory(true, null, null); assertThat(inv.getAllOwnedSkus().isEmpty()); } @Test public void shouldQueryInventoryWithSubscriptions() throws Exception { shouldStartSetup_SuccessCase(); Bundle response = createInventoryResponseBundle("foo"); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "subs", null)) .thenReturn(response); Inventory inventory = helper.queryInventory(false, null ,null); assertThat(inventory.getAllPurchases()).hasSize(1); assertThat(inventory.getAllOwnedSkus()).hasSize(1); assertThat(inventory.getSkuDetails()).isEmpty(); } @Test public void shouldQueryInventoryWithSubscriptionsAndSkus() throws Exception { shouldStartSetup_SuccessCase(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(createInventoryResponseBundle("foo")); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "subs", null)) .thenReturn(createInventoryResponseBundle("bar")); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), any(Bundle.class))) .thenReturn(createSkuDetailsResponseBundle("foo")); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("subs"), any(Bundle.class))) .thenReturn(createSkuDetailsResponseBundle("bar")); Inventory inventory = helper.queryInventory(true, null ,null); assertThat(inventory.getAllPurchases()).hasSize(2); assertThat(inventory.getAllOwnedSkus()).hasSize(2); assertThat(inventory.getSkuDetails()).hasSize(2); } @Test public void shouldQueryInventoryWithSubscriptionsAndSkusAndMoreSkus() throws Exception { shouldStartSetup_SuccessCase(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(createInventoryResponseBundle("inappSku")); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "subs", null)) .thenReturn(createInventoryResponseBundle("subSku")); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("inapp"), TestHelper.BundleStringArrayListMatcher.bundleWithStringValues(GET_SKU_DETAILS_ITEM_LIST, "inappSku", "anotherInAppSku"))) .thenReturn(createSkuDetailsResponseBundle("inappSku", "anotherInappSku")); when(service.getSkuDetails(eq(API_VERSION), eq(Robolectric.application.getPackageName()), eq("subs"), TestHelper.BundleStringArrayListMatcher.bundleWithStringValues(GET_SKU_DETAILS_ITEM_LIST, "subSku", "anotherSubSku"))) .thenReturn(createSkuDetailsResponseBundle("subSku", "anotherSubSku")); Inventory inventory = helper.queryInventory(true, asArrayList("anotherInAppSku"), asArrayList("anotherSubSku")); assertThat(inventory.getAllOwnedSkus(INAPP)).hasSize(1); assertThat(inventory.getAllOwnedSkus(SUBS)).hasSize(1); assertThat(inventory.getAllPurchases()).hasSize(2); // inappSku + subSku assertThat(inventory.getAllOwnedSkus()).hasSize(2); assertThat(inventory.getSkuDetails()).hasSize(4); } @Test(expected = IabException.class) public void shouldQueryInventoryRemoteException() throws Exception { shouldStartSetup_SuccessCase(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenThrow(new RemoteException()); helper.queryInventory(false, null, null); } @Test(expected = IabException.class) public void shouldQueryInventoryErrorCode() throws Exception { shouldStartSetup_SuccessCase(); Bundle response = new Bundle(); response.putInt(RESPONSE_CODE, DEVELOPER_ERROR.code); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); helper.queryInventory(false, null, null); } @Test(expected = IabException.class) public void shouldQueryInventoryJSONException() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = new Bundle(); response.putStringArrayList(RESPONSE_INAPP_ITEM_LIST, asArrayList("foo")); response.putStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST, asArrayList("not json")); response.putStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST, asArrayList("")); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); helper.queryInventory(false, null ,null); } @Test(expected = IabException.class) public void shouldQueryInventoryWithoutRequiredFields() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); Bundle response = new Bundle(); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); helper.queryInventory(false, null ,null); } @Test public void shouldQueryInventoryInvalidSignature() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); when(signatureValidator.validate(eq("{}"), eq("INVALID"))).thenReturn(false); Bundle response = new Bundle(); response.putStringArrayList(RESPONSE_INAPP_ITEM_LIST, asArrayList("foo")); response.putStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST, asArrayList("{}")); response.putStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST, asArrayList("INVALID")); response.putString(INAPP_CONTINUATION_TOKEN, ""); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); try { helper.queryInventory(false, null ,null); fail("expected exception"); } catch (IabException e) { assertThat(e.getResult()).isEqualTo(new IabResult(IABHELPER_VERIFICATION_FAILED, "Error refreshing inventory (querying owned items).")); } } @Test public void queryInventoryAsync() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); QueryInventoryFinishedListener listener = mock(QueryInventoryFinishedListener.class); Bundle response = createInventoryResponseBundle("foo"); when(service.getPurchases(API_VERSION, Robolectric.application.getPackageName(), "inapp", null)) .thenReturn(response); helper.queryInventoryAsync(false, null, null, listener); verify(listener).onQueryInventoryFinished(eq(new IabResult(OK)), any(Inventory.class)); } @Test public void queryInventoryAsyncWhenDisposed() throws Exception { shouldStartSetup_CheckForSubscriptions_Unavailable(); QueryInventoryFinishedListener listener = mock(QueryInventoryFinishedListener.class); helper.dispose(); helper.queryInventoryAsync(false, null, null, listener); verify(listener).onQueryInventoryFinished(eq(new IabResult(IABHELPER_DISPOSED)), isNull(Inventory.class)); } // consume @Test(expected = IabException.class) public void shouldNotConsumeSubscription() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getToken()).thenReturn("foo"); when(purchase.getItemType()).thenReturn(SUBS); helper.consume(purchase); } @Test(expected = IabException.class) public void shouldNotConsumePurchaseWithoutType() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getToken()).thenReturn("foo"); helper.consume(purchase); } @Test(expected = IabException.class) public void shouldNotConsumeInAppItemWithoutToken() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getItemType()).thenReturn(INAPP); helper.consume(purchase); } @Test(expected = IabException.class) public void shouldNotConsumeWhenDisposed() throws Exception { shouldStartSetup_SuccessCase(); helper.dispose(); Purchase purchase = mock(Purchase.class); when(purchase.getItemType()).thenReturn(INAPP); helper.consume(purchase); } @Test public void shouldConsumeInAppItem() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getToken()).thenReturn("foo"); when(purchase.getItemType()).thenReturn(INAPP); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "foo")).thenReturn(OK.code); helper.consume(purchase); } @Test(expected = IabException.class) public void shouldConsumeInAppItemErrorResponse() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getToken()).thenReturn("foo"); when(purchase.getItemType()).thenReturn(INAPP); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "foo")).thenReturn(ERROR.code); helper.consume(purchase); } @Test(expected = IabException.class) public void shouldConsumeInAppItemRemoteException() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "foo")).thenThrow(new RemoteException()); helper.consume(purchase); } @Test public void shouldConsumeAsyncSinglePurchase() throws Exception { shouldStartSetup_SuccessCase(); Purchase purchase = mock(Purchase.class); when(purchase.getToken()).thenReturn("foo"); when(purchase.getItemType()).thenReturn(INAPP); OnConsumeFinishedListener listener = mock(OnConsumeFinishedListener.class); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "foo")).thenReturn(OK.code); helper.consumeAsync(purchase, listener); verify(listener).onConsumeFinished(purchase, new IabResult(OK)); } @Test public void shouldConsumeAsyncMultiplePurchase() throws Exception { shouldStartSetup_SuccessCase(); Purchase p1 = mock(Purchase.class); when(p1.getToken()).thenReturn("foo"); when(p1.getItemType()).thenReturn(INAPP); Purchase p2 = mock(Purchase.class); when(p2.getToken()).thenReturn("bar"); when(p2.getItemType()).thenReturn(INAPP); List<Purchase> purchases = new ArrayList<Purchase>(); purchases.add(p1); purchases.add(p2); OnConsumeMultiFinishedListener listener = mock(OnConsumeMultiFinishedListener.class); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "foo")).thenReturn(OK.code); when(service.consumePurchase(API_VERSION, Robolectric.application.getPackageName(), "bar")).thenReturn(ERROR.code); helper.consumeAsync(purchases, listener); List<IabResult> results = new ArrayList<IabResult>(); results.add(new IabResult(OK)); results.add(new IabResult(ERROR)); verify(listener).onConsumeMultiFinished(purchases, results); } // getResponseCodeFromBundle @Test public void shouldGetResponseCodeFromBundleEmpty() throws Exception { Bundle b = new Bundle(); assertThat(helper.getResponseCodeFromBundle(b)).isEqualTo(OK.code); } @Test public void shouldGetResponseCodeFromBundleInt() throws Exception { Bundle b = new Bundle(); b.putInt(RESPONSE_CODE, 20); assertThat(helper.getResponseCodeFromBundle(b)).isEqualTo(20); } @Test public void shouldGetResponseCodeFromBundleLong() throws Exception { Bundle b = new Bundle(); b.putLong(RESPONSE_CODE, 60L); assertThat(helper.getResponseCodeFromBundle(b)).isEqualTo(60); } @Test(expected = RuntimeException.class) public void shouldGetResponseCodeFromBundleUnknown() throws Exception { Bundle b = new Bundle(); b.putString(RESPONSE_CODE, "invalid"); helper.getResponseCodeFromBundle(b); } // other methods @Test(expected = IllegalStateException.class) public void testFlagAsync() throws Exception { helper.flagStartAsync("something"); helper.flagEndAsync(); helper.flagStartAsync("something"); helper.flagStartAsync("something else"); } private Context registerServiceWithPackageManager() { Context context = Robolectric.application; RobolectricPackageManager pm = (RobolectricPackageManager) context.getPackageManager(); pm.addResolveInfoForIntent(IabHelper.BIND_BILLING_SERVICE, new ResolveInfo()); return context; } private static ArrayList<String> asArrayList(String... elements) { ArrayList<String> list = new ArrayList<String>(); Collections.addAll(list, elements); return list; } private static Bundle createInventoryResponseBundle(String... skus) { Bundle response = new Bundle(); ArrayList<String> itemList = new ArrayList<String>(skus.length); ArrayList<String> signatures = new ArrayList<String>(skus.length); ArrayList<String> purchaseData = new ArrayList<String>(skus.length); for (String sku : skus) { itemList.add(sku); signatures.add(""); purchaseData.add("{ \"productId\": \""+sku+"\" }"); } response.putInt(RESPONSE_CODE, OK.code); response.putStringArrayList(RESPONSE_INAPP_ITEM_LIST, itemList); response.putStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST, purchaseData); response.putStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST, signatures); response.putString(INAPP_CONTINUATION_TOKEN, ""); return response; } private static Bundle createSkuDetailsResponseBundle(String... skus) { Bundle response = new Bundle(); ArrayList<String> itemList = new ArrayList<String>(skus.length); for (String sku : skus) { itemList.add("{ \"productId\": \""+sku+"\" }"); } response.putInt(RESPONSE_CODE, OK.code); response.putStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST, itemList); return response; } final static String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzoFJ+dq/PQo2u71ndt2k\n" + "t0XK3oGFvUPagg0QogBrp2IyBKTodFtmcb0riKtDGjZ9JKB45GIBC3RR2fuC9lOR\n" + "15rRjA2Tfxoig0K/VYy7K5+fkLt2yGVDd3oqBFEDSGcwYYP1LfmgI8B2WJjACu3V\n" + "ehEQeO/cYrr8tav6VthmqdrL9C+BL9McTMjf3FzeJOTkiGeOFCu58T/sYvSc0ESG\n" + "YLh4lXAIG309WvEJ0GofxM4hWnD9aHcuu+hwYblrLJ5jk9hJQJmF7isripkDOQeO\n" + "9UbH0kNa9o1pq05beHmGW9a1pt3vWmgBQXZQIOKZzxvmh52d0BJWBgp7NMh68MSx\n" + "qwIDAQAB"; }