blob: 47bfc113c081bc6be64acf223fc84c4344e957e1 [file] [log] [blame]
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;
}
}