| package io.flutter.plugins.inapppurchase; |
| |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.END_CONNECTION; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; |
| import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; |
| import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; |
| import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; |
| import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; |
| import static java.util.Arrays.asList; |
| import static java.util.Collections.singletonList; |
| import static java.util.stream.Collectors.toList; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.contains; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doNothing; |
| 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 android.app.Activity; |
| import android.content.Context; |
| import androidx.annotation.Nullable; |
| import com.android.billingclient.api.BillingClient; |
| import com.android.billingclient.api.BillingClient.BillingResponse; |
| import com.android.billingclient.api.BillingClient.SkuType; |
| import com.android.billingclient.api.BillingClientStateListener; |
| import com.android.billingclient.api.BillingFlowParams; |
| import com.android.billingclient.api.ConsumeResponseListener; |
| import com.android.billingclient.api.Purchase; |
| import com.android.billingclient.api.Purchase.PurchasesResult; |
| import com.android.billingclient.api.PurchaseHistoryResponseListener; |
| import com.android.billingclient.api.SkuDetails; |
| import com.android.billingclient.api.SkuDetailsParams; |
| import com.android.billingclient.api.SkuDetailsResponseListener; |
| import io.flutter.plugin.common.MethodCall; |
| import io.flutter.plugin.common.MethodChannel; |
| import io.flutter.plugin.common.MethodChannel.Result; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.mockito.Spy; |
| |
| public class MethodCallHandlerTest { |
| private MethodCallHandlerImpl methodChannelHandler; |
| private BillingClientFactory factory; |
| @Mock BillingClient mockBillingClient; |
| @Mock MethodChannel mockMethodChannel; |
| @Spy Result result; |
| @Mock Activity activity; |
| @Mock Context context; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| |
| factory = (context, channel) -> mockBillingClient; |
| methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); |
| } |
| |
| @Test |
| public void invalidMethod() { |
| MethodCall call = new MethodCall("invalid", null); |
| methodChannelHandler.onMethodCall(call, result); |
| verify(result, times(1)).notImplemented(); |
| } |
| |
| @Test |
| public void isReady_true() { |
| mockStartConnection(); |
| MethodCall call = new MethodCall(IS_READY, null); |
| when(mockBillingClient.isReady()).thenReturn(true); |
| methodChannelHandler.onMethodCall(call, result); |
| verify(result).success(true); |
| } |
| |
| @Test |
| public void isReady_false() { |
| mockStartConnection(); |
| MethodCall call = new MethodCall(IS_READY, null); |
| when(mockBillingClient.isReady()).thenReturn(false); |
| methodChannelHandler.onMethodCall(call, result); |
| verify(result).success(false); |
| } |
| |
| @Test |
| public void isReady_clientDisconnected() { |
| MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); |
| methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); |
| MethodCall isReadyCall = new MethodCall(IS_READY, null); |
| |
| methodChannelHandler.onMethodCall(isReadyCall, result); |
| |
| verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void startConnection() { |
| ArgumentCaptor<BillingClientStateListener> captor = mockStartConnection(); |
| verify(result, never()).success(any()); |
| captor.getValue().onBillingSetupFinished(100); |
| |
| verify(result, times(1)).success(100); |
| } |
| |
| @Test |
| public void startConnection_multipleCalls() { |
| Map<String, Integer> arguments = new HashMap<>(); |
| arguments.put("handle", 1); |
| MethodCall call = new MethodCall(START_CONNECTION, arguments); |
| ArgumentCaptor<BillingClientStateListener> captor = |
| ArgumentCaptor.forClass(BillingClientStateListener.class); |
| doNothing().when(mockBillingClient).startConnection(captor.capture()); |
| |
| methodChannelHandler.onMethodCall(call, result); |
| verify(result, never()).success(any()); |
| captor.getValue().onBillingSetupFinished(100); |
| captor.getValue().onBillingSetupFinished(200); |
| captor.getValue().onBillingSetupFinished(300); |
| |
| verify(result, times(1)).success(100); |
| verify(result, times(1)).success(any()); |
| } |
| |
| @Test |
| public void endConnection() { |
| // Set up a connected BillingClient instance |
| final int disconnectCallbackHandle = 22; |
| Map<String, Integer> arguments = new HashMap<>(); |
| arguments.put("handle", disconnectCallbackHandle); |
| MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); |
| ArgumentCaptor<BillingClientStateListener> captor = |
| ArgumentCaptor.forClass(BillingClientStateListener.class); |
| doNothing().when(mockBillingClient).startConnection(captor.capture()); |
| methodChannelHandler.onMethodCall(connectCall, mock(Result.class)); |
| final BillingClientStateListener stateListener = captor.getValue(); |
| |
| // Disconnect the connected client |
| MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); |
| methodChannelHandler.onMethodCall(disconnectCall, result); |
| |
| // Verify that the client is disconnected and that the OnDisconnect callback has |
| // been triggered |
| verify(result, times(1)).success(any()); |
| verify(mockBillingClient, times(1)).endConnection(); |
| stateListener.onBillingServiceDisconnected(); |
| Map<String, Integer> expectedInvocation = new HashMap<>(); |
| expectedInvocation.put("handle", disconnectCallbackHandle); |
| verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); |
| } |
| |
| @Test |
| public void querySkuDetailsAsync() { |
| // Connect a billing client and set up the SKU query listeners |
| establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); |
| String skuType = BillingClient.SkuType.INAPP; |
| List<String> skusList = asList("id1", "id2"); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", skuType); |
| arguments.put("skusList", skusList); |
| MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); |
| |
| // Query for SKU details |
| methodChannelHandler.onMethodCall(queryCall, result); |
| |
| // Assert the arguments were forwarded correctly to BillingClient |
| ArgumentCaptor<SkuDetailsParams> paramCaptor = ArgumentCaptor.forClass(SkuDetailsParams.class); |
| ArgumentCaptor<SkuDetailsResponseListener> listenerCaptor = |
| ArgumentCaptor.forClass(SkuDetailsResponseListener.class); |
| verify(mockBillingClient).querySkuDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); |
| assertEquals(paramCaptor.getValue().getSkuType(), skuType); |
| assertEquals(paramCaptor.getValue().getSkusList(), skusList); |
| |
| // Assert that we handed result BillingClient's response |
| int responseCode = 200; |
| List<SkuDetails> skuDetailsResponse = asList(buildSkuDetails("foo")); |
| listenerCaptor.getValue().onSkuDetailsResponse(responseCode, skuDetailsResponse); |
| ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class); |
| verify(result).success(resultCaptor.capture()); |
| HashMap<String, Object> resultData = resultCaptor.getValue(); |
| assertEquals(resultData.get("responseCode"), responseCode); |
| assertEquals(resultData.get("skuDetailsList"), fromSkuDetailsList(skuDetailsResponse)); |
| } |
| |
| @Test |
| public void querySkuDetailsAsync_clientDisconnected() { |
| // Disconnect the Billing client and prepare a querySkuDetails call |
| MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); |
| methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); |
| String skuType = BillingClient.SkuType.INAPP; |
| List<String> skusList = asList("id1", "id2"); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", skuType); |
| arguments.put("skusList", skusList); |
| MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); |
| |
| // Query for SKU details |
| methodChannelHandler.onMethodCall(queryCall, result); |
| |
| // Assert that we sent an error back. |
| verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void launchBillingFlow_ok_nullAccountId() { |
| // Fetch the sku details first and then prepare the launch billing flow call |
| String skuId = "foo"; |
| queryForSkus(singletonList(skuId)); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("sku", skuId); |
| arguments.put("accountId", null); |
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); |
| |
| // Launch the billing flow |
| int responseCode = BillingResponse.OK; |
| when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(responseCode); |
| methodChannelHandler.onMethodCall(launchCall, result); |
| |
| // Verify we pass the arguments to the billing flow |
| ArgumentCaptor<BillingFlowParams> billingFlowParamsCaptor = |
| ArgumentCaptor.forClass(BillingFlowParams.class); |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); |
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); |
| assertEquals(params.getSku(), skuId); |
| assertNull(params.getAccountId()); |
| |
| // Verify we pass the response code to result |
| verify(result, never()).error(any(), any(), any()); |
| verify(result, times(1)).success(responseCode); |
| } |
| |
| @Test |
| public void launchBillingFlow_ok_null_Activity() { |
| methodChannelHandler.setActivity(null); |
| |
| // Fetch the sku details first and then prepare the launch billing flow call |
| String skuId = "foo"; |
| String accountId = "account"; |
| queryForSkus(singletonList(skuId)); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("sku", skuId); |
| arguments.put("accountId", accountId); |
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); |
| methodChannelHandler.onMethodCall(launchCall, result); |
| |
| // Verify we pass the response code to result |
| verify(result).error(contains("ACTIVITY_UNAVAILABLE"), contains("foreground"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void launchBillingFlow_ok_AccountId() { |
| // Fetch the sku details first and query the method call |
| String skuId = "foo"; |
| String accountId = "account"; |
| queryForSkus(singletonList(skuId)); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("sku", skuId); |
| arguments.put("accountId", accountId); |
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); |
| |
| // Launch the billing flow |
| int responseCode = BillingResponse.OK; |
| when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(responseCode); |
| methodChannelHandler.onMethodCall(launchCall, result); |
| |
| // Verify we pass the arguments to the billing flow |
| ArgumentCaptor<BillingFlowParams> billingFlowParamsCaptor = |
| ArgumentCaptor.forClass(BillingFlowParams.class); |
| verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); |
| BillingFlowParams params = billingFlowParamsCaptor.getValue(); |
| assertEquals(params.getSku(), skuId); |
| assertEquals(params.getAccountId(), accountId); |
| |
| // Verify we pass the response code to result |
| verify(result, never()).error(any(), any(), any()); |
| verify(result, times(1)).success(responseCode); |
| } |
| |
| @Test |
| public void launchBillingFlow_clientDisconnected() { |
| // Prepare the launch call after disconnecting the client |
| MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); |
| methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); |
| String skuId = "foo"; |
| String accountId = "account"; |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("sku", skuId); |
| arguments.put("accountId", accountId); |
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); |
| |
| methodChannelHandler.onMethodCall(launchCall, result); |
| |
| // Assert that we sent an error back. |
| verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void launchBillingFlow_skuNotFound() { |
| // Try to launch the billing flow for a random sku ID |
| establishConnectedBillingClient(null, null); |
| String skuId = "foo"; |
| String accountId = "account"; |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("sku", skuId); |
| arguments.put("accountId", accountId); |
| MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); |
| |
| methodChannelHandler.onMethodCall(launchCall, result); |
| |
| // Assert that we sent an error back. |
| verify(result).error(contains("NOT_FOUND"), contains(skuId), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void queryPurchases() { |
| establishConnectedBillingClient(null, null); |
| PurchasesResult purchasesResult = mock(PurchasesResult.class); |
| when(purchasesResult.getResponseCode()).thenReturn(BillingResponse.OK); |
| Purchase purchase = buildPurchase("foo"); |
| when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase)); |
| when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult); |
| |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", SkuType.INAPP); |
| methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); |
| |
| // Verify we pass the response to result |
| ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class); |
| verify(result, never()).error(any(), any(), any()); |
| verify(result, times(1)).success(resultCaptor.capture()); |
| assertEquals(fromPurchasesResult(purchasesResult), resultCaptor.getValue()); |
| } |
| |
| @Test |
| public void queryPurchases_clientDisconnected() { |
| // Prepare the launch call after disconnecting the client |
| methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); |
| |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", SkuType.INAPP); |
| methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); |
| |
| // Assert that we sent an error back. |
| verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void queryPurchaseHistoryAsync() { |
| // Set up an established billing client and all our mocked responses |
| establishConnectedBillingClient(null, null); |
| ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class); |
| int responseCode = BillingResponse.OK; |
| List<Purchase> purchasesList = asList(buildPurchase("foo")); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", SkuType.INAPP); |
| ArgumentCaptor<PurchaseHistoryResponseListener> listenerCaptor = |
| ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); |
| |
| methodChannelHandler.onMethodCall( |
| new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); |
| |
| // Verify we pass the data to result |
| verify(mockBillingClient) |
| .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture()); |
| listenerCaptor.getValue().onPurchaseHistoryResponse(responseCode, purchasesList); |
| verify(result).success(resultCaptor.capture()); |
| HashMap<String, Object> resultData = resultCaptor.getValue(); |
| assertEquals(responseCode, resultData.get("responseCode")); |
| assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); |
| } |
| |
| @Test |
| public void queryPurchaseHistoryAsync_clientDisconnected() { |
| // Prepare the launch call after disconnecting the client |
| methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); |
| |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("skuType", SkuType.INAPP); |
| methodChannelHandler.onMethodCall( |
| new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); |
| |
| // Assert that we sent an error back. |
| verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); |
| verify(result, never()).success(any()); |
| } |
| |
| @Test |
| public void onPurchasesUpdatedListener() { |
| PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); |
| |
| int responseCode = BillingResponse.OK; |
| List<Purchase> purchasesList = asList(buildPurchase("foo")); |
| ArgumentCaptor<HashMap<String, Object>> resultCaptor = ArgumentCaptor.forClass(HashMap.class); |
| doNothing() |
| .when(mockMethodChannel) |
| .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); |
| listener.onPurchasesUpdated(responseCode, purchasesList); |
| |
| HashMap<String, Object> resultData = resultCaptor.getValue(); |
| assertEquals(responseCode, resultData.get("responseCode")); |
| assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); |
| } |
| |
| @Test |
| public void consumeAsync() { |
| establishConnectedBillingClient(null, null); |
| ArgumentCaptor<BillingResponse> resultCaptor = ArgumentCaptor.forClass(BillingResponse.class); |
| int responseCode = BillingResponse.OK; |
| HashMap<String, Object> arguments = new HashMap<>(); |
| arguments.put("purchaseToken", "mockToken"); |
| ArgumentCaptor<ConsumeResponseListener> listenerCaptor = |
| ArgumentCaptor.forClass(ConsumeResponseListener.class); |
| |
| methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); |
| |
| // Verify we pass the data to result |
| verify(mockBillingClient).consumeAsync(eq("mockToken"), listenerCaptor.capture()); |
| |
| listenerCaptor.getValue().onConsumeResponse(responseCode, "mockToken"); |
| verify(result).success(resultCaptor.capture()); |
| |
| // Verify we pass the response code to result |
| verify(result, never()).error(any(), any(), any()); |
| verify(result, times(1)).success(responseCode); |
| } |
| |
| private ArgumentCaptor<BillingClientStateListener> mockStartConnection() { |
| Map<String, Integer> arguments = new HashMap<>(); |
| arguments.put("handle", 1); |
| MethodCall call = new MethodCall(START_CONNECTION, arguments); |
| ArgumentCaptor<BillingClientStateListener> captor = |
| ArgumentCaptor.forClass(BillingClientStateListener.class); |
| doNothing().when(mockBillingClient).startConnection(captor.capture()); |
| |
| methodChannelHandler.onMethodCall(call, result); |
| return captor; |
| } |
| |
| private void establishConnectedBillingClient( |
| @Nullable Map<String, Integer> arguments, @Nullable Result result) { |
| if (arguments == null) { |
| arguments = new HashMap<>(); |
| arguments.put("handle", 1); |
| } |
| if (result == null) { |
| result = mock(Result.class); |
| } |
| |
| MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); |
| methodChannelHandler.onMethodCall(connectCall, result); |
| } |
| |
| private void queryForSkus(List<String> skusList) { |
| // Set up the query method call |
| establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); |
| HashMap<String, Object> arguments = new HashMap<>(); |
| String skuType = SkuType.INAPP; |
| arguments.put("skuType", skuType); |
| arguments.put("skusList", skusList); |
| MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); |
| |
| // Call the method. |
| methodChannelHandler.onMethodCall(queryCall, mock(Result.class)); |
| |
| // Respond to the call with a matching set of Sku details. |
| ArgumentCaptor<SkuDetailsResponseListener> listenerCaptor = |
| ArgumentCaptor.forClass(SkuDetailsResponseListener.class); |
| verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); |
| List<SkuDetails> skuDetailsResponse = |
| skusList.stream().map(this::buildSkuDetails).collect(toList()); |
| listenerCaptor.getValue().onSkuDetailsResponse(BillingResponse.OK, skuDetailsResponse); |
| } |
| |
| private SkuDetails buildSkuDetails(String id) { |
| SkuDetails details = mock(SkuDetails.class); |
| when(details.getSku()).thenReturn(id); |
| return details; |
| } |
| |
| private Purchase buildPurchase(String orderId) { |
| Purchase purchase = mock(Purchase.class); |
| when(purchase.getOrderId()).thenReturn(orderId); |
| return purchase; |
| } |
| } |