diff --git a/packages/in_app_purchase/android/build.gradle b/packages/in_app_purchase/android/build.gradle index 8d5840b4daff..7573c0d3ecad 100644 --- a/packages/in_app_purchase/android/build.gradle +++ b/packages/in_app_purchase/android/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.0.0' implementation 'com.android.billingclient:billing:3.0.2' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.6.0' + testImplementation 'org.mockito:mockito-inline:3.6.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index d90fc6040454..b10845220dfb 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -15,6 +15,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; @@ -43,14 +44,14 @@ class MethodCallHandlerImpl private static final String LOAD_SKU_DOC_URL = "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale"; - @Nullable private BillingClient billingClient; + @VisibleForTesting @Nullable BillingClient billingClient; private final BillingClientFactory billingClientFactory; @Nullable private Activity activity; private final Context applicationContext; private final MethodChannel methodChannel; - private HashMap cachedSkus = new HashMap<>(); + @VisibleForTesting HashMap cachedSkus = new HashMap<>(); /** Constructs the MethodCallHandlerImpl */ MethodCallHandlerImpl( diff --git a/packages/in_app_purchase/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java new file mode 100644 index 000000000000..078e3268b879 --- /dev/null +++ b/packages/in_app_purchase/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -0,0 +1,198 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchase; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +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 com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.SkuDetails; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class MethodCallHandlerTest { + private MethodCallHandlerImpl methodCallHandler; + private MockedStatic mockStaticBuilder; + private BillingFlowParams.Builder billingFlowParamsBuilder; + private BillingFlowParams billingFlowParams; + + @Before + public void before() { + methodCallHandler = + new MethodCallHandlerImpl( + null, mock(Context.class), mock(MethodChannel.class), mock(BillingClientFactory.class)); + methodCallHandler.billingClient = mock(BillingClient.class); + BillingClient client = mock(BillingClient.class); + when(client.isReady()).thenReturn(true); + + SkuDetails details = mock(SkuDetails.class); + methodCallHandler.cachedSkus.put("testPurchase", details); + + setupBillingFlow(); + } + + @After + public void after() { + closeBillingFlow(); + } + + @Test + public void isReady_returns_true_if_billingClientIsReady() { + BillingClient client = mock(BillingClient.class); + when(client.isReady()).thenReturn(true); + MethodChannel.Result result = mock(MethodChannel.Result.class); + MethodCall call = new MethodCall(InAppPurchasePlugin.MethodNames.IS_READY, null); + + methodCallHandler.billingClient = client; + methodCallHandler.onMethodCall(call, result); + + verify(result, times(1)).success(true); + } + + @Test + public void isReady_returns_false_if_billingClientIsNotReady() { + BillingClient client = mock(BillingClient.class); + when(client.isReady()).thenReturn(false); + MethodCall call = new MethodCall(InAppPurchasePlugin.MethodNames.IS_READY, null); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + methodCallHandler.billingClient = client; + methodCallHandler.onMethodCall(call, result); + + verify(result, times(1)).success(false); + } + + @Test + public void isReady_returns_false_if_billingClientIsNotSet() { + MethodCall call = new MethodCall(InAppPurchasePlugin.MethodNames.IS_READY, null); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + methodCallHandler.billingClient = null; + methodCallHandler.onMethodCall(call, result); + + verify(result, times(1)) + .error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); + } + + private Map createBillingArgs() { + Map args = new HashMap<>(); + args.put("sku", "testPurchase"); + return args; + } + + @Test + public void launchBillingFlow_fails_withMissingSku() { + MethodCall call = + new MethodCall(InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW, createBillingArgs()); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + methodCallHandler.cachedSkus.remove("testPurchase"); + methodCallHandler.onMethodCall(call, result); + + verify(result, times(1)) + .error( + "NOT_FOUND", + "Details for sku testPurchase are not available. " + + "It might because skus were not fetched prior to the call. " + + "Please fetch the skus first. An example of how to fetch the skus could be found here: " + + "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale", + null); + } + + @Test + public void launchBillingFlow_fails_whenNotAttachedToActivity() { + MethodCall call = + new MethodCall(InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW, createBillingArgs()); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + methodCallHandler.onMethodCall(call, result); + + verify(result, times(1)) + .error( + "ACTIVITY_UNAVAILABLE", + "Details for sku testPurchase are not available. This method must be run with the app in foreground.", + null); + } + + @Test + public void launchBillingFlow_launchesBillingFlowOnClient() { + MethodCall call = + new MethodCall(InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW, createBillingArgs()); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + assert methodCallHandler.billingClient != null; + when(methodCallHandler.billingClient.launchBillingFlow( + any(Activity.class), any(BillingFlowParams.class))) + .thenReturn(mock(BillingResult.class)); + + Activity activity = mock(Activity.class); + methodCallHandler.setActivity(activity); + methodCallHandler.onMethodCall(call, result); + + verify(methodCallHandler.billingClient, times(1)) + .launchBillingFlow(activity, billingFlowParams); + verify(result, times(1)).success(any()); + } + + @Test + public void launchBillingFlow_setsObfuscatedProfileId() { + String obfuscatedProfileId = "abcdef"; + Map billingArgs = createBillingArgs(); + billingArgs.put("obfuscatedProfileId", obfuscatedProfileId); + + MethodCall call = + new MethodCall(InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW, billingArgs); + MethodChannel.Result result = mock(MethodChannel.Result.class); + + methodCallHandler.setActivity(mock(Activity.class)); + methodCallHandler.onMethodCall(call, result); + + verify(billingFlowParamsBuilder, times(1)).setObfuscatedProfileId(obfuscatedProfileId); + } + + private void setupBillingFlow() { + assert methodCallHandler.billingClient != null; + when(methodCallHandler.billingClient.launchBillingFlow( + any(Activity.class), any(BillingFlowParams.class))) + .thenReturn(mock(BillingResult.class)); + + billingFlowParamsBuilder = mock(BillingFlowParams.Builder.class); + mockStaticBuilder = mockStatic(BillingFlowParams.class); + mockStaticBuilder + .when( + new MockedStatic.Verification() { + @Override + public void apply() { + BillingFlowParams.newBuilder(); + } + }) + .thenReturn(billingFlowParamsBuilder); + + when(billingFlowParamsBuilder.setSkuDetails(any(SkuDetails.class))) + .thenReturn(billingFlowParamsBuilder); + when(billingFlowParamsBuilder.setObfuscatedAccountId(any(String.class))) + .thenReturn(billingFlowParamsBuilder); + billingFlowParams = mock(BillingFlowParams.class); + when(billingFlowParamsBuilder.build()).thenReturn(billingFlowParams); + } + + private void closeBillingFlow() { + mockStaticBuilder.close(); + } +}