| // Copyright 2020 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 org.chromium.chrome.browser.payments; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.Parcelable; |
| import android.text.TextUtils; |
| |
| import androidx.test.filters.MediumTest; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.ExpectedException; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.test.util.CommandLineFlags; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.flags.ChromeSwitches; |
| import org.chromium.chrome.test.ChromeActivityTestRule; |
| import org.chromium.chrome.test.ChromeJUnit4ClassRunner; |
| import org.chromium.chrome.test.util.browser.Features.EnableFeatures; |
| import org.chromium.components.payments.Address; |
| import org.chromium.components.payments.ErrorStrings; |
| import org.chromium.components.payments.IPaymentDetailsUpdateService; |
| import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback; |
| import org.chromium.components.payments.PaymentDetailsUpdateService; |
| import org.chromium.components.payments.PaymentDetailsUpdateServiceHelper; |
| import org.chromium.components.payments.PaymentFeatureList; |
| import org.chromium.components.payments.PaymentRequestUpdateEventListener; |
| import org.chromium.components.payments.intent.WebPaymentIntentHelperType.PaymentCurrencyAmount; |
| import org.chromium.components.payments.intent.WebPaymentIntentHelperType.PaymentHandlerMethodData; |
| import org.chromium.components.payments.intent.WebPaymentIntentHelperType.PaymentRequestDetailsUpdate; |
| import org.chromium.components.payments.intent.WebPaymentIntentHelperType.PaymentShippingOption; |
| import org.chromium.content_public.browser.test.util.CriteriaHelper; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.payments.mojom.PaymentAddress; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Tests for PaymentDetailsUpdateServiceHelper. |
| **/ |
| @RunWith(ChromeJUnit4ClassRunner.class) |
| @EnableFeatures({PaymentFeatureList.ANDROID_APP_PAYMENT_UPDATE_EVENTS}) |
| @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) |
| public class PaymentDetailsUpdateServiceHelperTest { |
| private static final int DECODER_STARTUP_TIMEOUT_IN_MS = 10000; |
| |
| @Rule |
| public ChromeActivityTestRule<ChromeActivity> mRule = |
| new ChromeActivityTestRule<>(ChromeActivity.class); |
| |
| @Rule |
| public ExpectedException thrown = ExpectedException.none(); |
| |
| /** Simulates a package manager in memory. */ |
| private final MockPackageManagerDelegate mPackageManager = new MockPackageManagerDelegate(); |
| |
| private Context mContext; |
| |
| private Bundle defaultAddressBundle() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(Address.EXTRA_ADDRESS_COUNTRY, "CA"); |
| String[] addressLine = {"111 Richmond Street West"}; |
| bundle.putStringArray(Address.EXTRA_ADDRESS_LINES, addressLine); |
| bundle.putString(Address.EXTRA_ADDRESS_REGION, "Ontario"); |
| bundle.putString(Address.EXTRA_ADDRESS_CITY, "Toronto"); |
| bundle.putString(Address.EXTRA_ADDRESS_POSTAL_CODE, "M5H2G4"); |
| bundle.putString(Address.EXTRA_ADDRESS_RECIPIENT, "John Smith"); |
| bundle.putString(Address.EXTRA_ADDRESS_PHONE, "4169158200"); |
| return bundle; |
| } |
| |
| private Bundle defaultMethodDataBundle() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(PaymentHandlerMethodData.EXTRA_METHOD_NAME, "method-name"); |
| bundle.putString( |
| PaymentHandlerMethodData.EXTRA_STRINGIFIED_DETAILS, "{\"key\": \"value\"}"); |
| return bundle; |
| } |
| |
| private boolean mBound; |
| private IPaymentDetailsUpdateService mIPaymentDetailsUpdateService; |
| private ServiceConnection mConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| mIPaymentDetailsUpdateService = IPaymentDetailsUpdateService.Stub.asInterface(service); |
| mBound = true; |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName className) { |
| mIPaymentDetailsUpdateService = null; |
| mBound = false; |
| } |
| }; |
| |
| @Before |
| public void setUp() throws Throwable { |
| mRule.startMainActivityOnBlankPage(); |
| mContext = mRule.getActivity(); |
| } |
| |
| private void installPaymentApp() { |
| mPackageManager.installPaymentApp( |
| "BobPay", /*packageName=*/"com.bobpay", null /* no metadata */, /*signature=*/"01"); |
| |
| mPackageManager.setInvokedAppPackageName(/*packageName=*/"com.bobpay"); |
| } |
| |
| private void installAndInvokePaymentApp() throws Throwable { |
| installPaymentApp(); |
| mRule.runOnUiThread(() -> { |
| PaymentDetailsUpdateServiceHelper.getInstance().initialize( |
| mPackageManager, /*packageName=*/"com.bobpay", mUpdateListener); |
| }); |
| } |
| |
| private void updateWithDefaultDetails() throws Throwable { |
| PaymentCurrencyAmount total = |
| new PaymentCurrencyAmount(/*currency=*/"CAD", /*value=*/"10.00"); |
| |
| // Populate shipping options. |
| List<PaymentShippingOption> shippingOptions = new ArrayList<PaymentShippingOption>(); |
| shippingOptions.add(new PaymentShippingOption( |
| "shippingId", "Free shipping", "CAD", "0.00", /*selected=*/true)); |
| |
| // Populate address errors. |
| Bundle bundledShippingAddressErrors = new Bundle(); |
| bundledShippingAddressErrors.putString("addressLine", "invalid address line"); |
| bundledShippingAddressErrors.putString("city", "invalid city"); |
| bundledShippingAddressErrors.putString("countryCode", "invalid country code"); |
| bundledShippingAddressErrors.putString("dependentLocality", "invalid dependent locality"); |
| bundledShippingAddressErrors.putString("organization", "invalid organization"); |
| bundledShippingAddressErrors.putString("phone", "invalid phone"); |
| bundledShippingAddressErrors.putString("postalCode", "invalid postal code"); |
| bundledShippingAddressErrors.putString("recipient", "invalid recipient"); |
| bundledShippingAddressErrors.putString("region", "invalid region"); |
| bundledShippingAddressErrors.putString("sortingCode", "invalid sorting code"); |
| |
| PaymentRequestDetailsUpdate response = |
| new PaymentRequestDetailsUpdate(total, shippingOptions, |
| /*error=*/"error message", |
| /*pstringifiedPaymentMethodErrors=*/"stringified payment method", |
| bundledShippingAddressErrors); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { PaymentDetailsUpdateServiceHelper.getInstance().updateWith(response); }); |
| } |
| |
| private void onPaymentDetailsNotUpdated() throws Throwable { |
| TestThreadUtils.runOnUiThreadBlocking(() -> { |
| PaymentDetailsUpdateServiceHelper.getInstance().onPaymentDetailsNotUpdated(); |
| }); |
| } |
| |
| private void verifyUpdatedDefaultDetails() { |
| Bundle total = mUpdatedPaymentDetails.getBundle(PaymentRequestDetailsUpdate.EXTRA_TOTAL); |
| Assert.assertEquals("CAD", total.getString(PaymentCurrencyAmount.EXTRA_CURRENCY)); |
| Assert.assertEquals("10.00", total.getString(PaymentCurrencyAmount.EXTRA_VALUE)); |
| |
| // Validate shipping options |
| Parcelable[] shippingOptions = mUpdatedPaymentDetails.getParcelableArray( |
| PaymentRequestDetailsUpdate.EXTRA_SHIPPING_OPTIONS); |
| Assert.assertEquals(1, shippingOptions.length); |
| Bundle shippingOption = (Bundle) shippingOptions[0]; |
| Assert.assertEquals("shippingId", |
| shippingOption.getString(PaymentShippingOption.EXTRA_SHIPPING_OPTION_ID)); |
| Assert.assertEquals("Free shipping", |
| shippingOption.getString(PaymentShippingOption.EXTRA_SHIPPING_OPTION_LABEL)); |
| Assert.assertEquals("CAD", |
| shippingOption.getString( |
| PaymentShippingOption.EXTRA_SHIPPING_OPTION_AMOUNT_CURRENCY)); |
| Assert.assertEquals("0.00", |
| shippingOption.getString(PaymentShippingOption.EXTRA_SHIPPING_OPTION_AMOUNT_VALUE)); |
| Assert.assertTrue( |
| shippingOption.getBoolean(PaymentShippingOption.EXTRA_SHIPPING_OPTION_SELECTED)); |
| |
| Assert.assertEquals("error message", |
| mUpdatedPaymentDetails.getString(PaymentRequestDetailsUpdate.EXTRA_ERROR_MESSAGE)); |
| Assert.assertEquals("stringified payment method", |
| mUpdatedPaymentDetails.getString( |
| PaymentRequestDetailsUpdate.EXTRA_STRINGIFIED_PAYMENT_METHOD_ERRORS)); |
| |
| // Validate address errors |
| Bundle addressError = |
| mUpdatedPaymentDetails.getBundle(PaymentRequestDetailsUpdate.EXTRA_ADDRESS_ERRORS); |
| Assert.assertEquals("invalid address line", addressError.getString("addressLine")); |
| Assert.assertEquals("invalid city", addressError.getString("city")); |
| Assert.assertEquals("invalid country code", addressError.getString("countryCode")); |
| Assert.assertEquals( |
| "invalid dependent locality", addressError.getString("dependentLocality")); |
| Assert.assertEquals("invalid organization", addressError.getString("organization")); |
| Assert.assertEquals("invalid phone", addressError.getString("phone")); |
| Assert.assertEquals("invalid postal code", addressError.getString("postalCode")); |
| Assert.assertEquals("invalid recipient", addressError.getString("recipient")); |
| Assert.assertEquals("invalid region", addressError.getString("region")); |
| Assert.assertEquals("invalid sorting code", addressError.getString("sortingCode")); |
| } |
| |
| private void startPaymentDetailsUpdateService() { |
| Intent intent = new Intent(/*ContextUtils.getApplicationContext()*/ mContext, |
| PaymentDetailsUpdateService.class); |
| intent.setAction(IPaymentDetailsUpdateService.class.getName()); |
| mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); |
| |
| CriteriaHelper.pollUiThread(() -> { |
| return mBound; |
| }, DECODER_STARTUP_TIMEOUT_IN_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); |
| } |
| |
| private boolean mMethodChangeListenerNotified; |
| private boolean mShippingOptionChangeListenerNotified; |
| private boolean mShippingAddressChangeListenerNotified; |
| private PaymentRequestUpdateEventListener mUpdateListener = |
| new FakePaymentRequestUpdateEventListener(); |
| private class FakePaymentRequestUpdateEventListener |
| implements PaymentRequestUpdateEventListener { |
| @Override |
| public boolean changePaymentMethodFromInvokedApp( |
| String methodName, String stringifiedDetails) { |
| Assert.assertFalse(TextUtils.isEmpty(methodName)); |
| mMethodChangeListenerNotified = true; |
| return true; |
| } |
| @Override |
| public boolean changeShippingOptionFromInvokedApp(String shippingOptionId) { |
| Assert.assertFalse(TextUtils.isEmpty(shippingOptionId)); |
| mShippingOptionChangeListenerNotified = true; |
| return true; |
| } |
| @Override |
| public boolean changeShippingAddressFromInvokedApp(PaymentAddress shippingAddress) { |
| mShippingAddressChangeListenerNotified = true; |
| return true; |
| } |
| } |
| |
| private Bundle mUpdatedPaymentDetails; |
| private boolean mPaymentDetailsDidNotUpdate; |
| private class PaymentDetailsUpdateServiceCallback |
| extends IPaymentDetailsUpdateServiceCallback.Stub { |
| @Override |
| public void updateWith(Bundle updatedPaymentDetails) { |
| mUpdatedPaymentDetails = updatedPaymentDetails; |
| } |
| |
| @Override |
| public void paymentDetailsNotUpdated() { |
| mPaymentDetailsDidNotUpdate = true; |
| } |
| } |
| |
| private String receivedErrorString() { |
| return mUpdatedPaymentDetails.getString( |
| PaymentRequestDetailsUpdate.EXTRA_ERROR_MESSAGE, ""); |
| } |
| |
| private void verifyIsWaitingForPaymentDetailsUpdate(boolean expected) { |
| TestThreadUtils.runOnUiThreadBlocking(() -> { |
| Assert.assertEquals(expected, |
| PaymentDetailsUpdateServiceHelper.getInstance() |
| .isWaitingForPaymentDetailsUpdate()); |
| }); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testConnectWhenPaymentAppNotInvoked() throws Throwable { |
| installPaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| defaultMethodDataBundle(), new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mMethodChangeListenerNotified); |
| // An unauthorized app won't get a callback with error. |
| Assert.assertEquals(null, mUpdatedPaymentDetails); |
| Assert.assertFalse(mPaymentDetailsDidNotUpdate); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testSuccessfulChangePaymentMethod() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| defaultMethodDataBundle(), new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mMethodChangeListenerNotified); |
| updateWithDefaultDetails(); |
| verifyUpdatedDefaultDetails(); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangePaymentMethodMissingBundle() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| null, new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mMethodChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.METHOD_DATA_REQUIRED, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangePaymentMethodMissingMethodNameBundle() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| Bundle bundle = new Bundle(); |
| bundle.putString( |
| PaymentHandlerMethodData.EXTRA_STRINGIFIED_DETAILS, "{\"key\": \"value\"}"); |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| bundle, new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mMethodChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.METHOD_NAME_REQUIRED, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testSuccessfulChangePaymentMethodWithMissingDetails() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| Bundle bundle = new Bundle(); |
| bundle.putString(PaymentHandlerMethodData.EXTRA_METHOD_NAME, "method-name"); |
| // Skip populating "PaymentHandlerMethodData.EXTRA_STRINGIFIED_DETAILS" to verify that it is |
| // not a mandatory field. |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| bundle, new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mMethodChangeListenerNotified); |
| updateWithDefaultDetails(); |
| verifyUpdatedDefaultDetails(); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testSuccessfulChangeShippingOption() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changeShippingOption( |
| "shipping option id", new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mShippingOptionChangeListenerNotified); |
| updateWithDefaultDetails(); |
| verifyUpdatedDefaultDetails(); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangeShippingOptionWithMissingOptionId() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changeShippingOption( |
| /*shippingOptionId=*/"", new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mShippingOptionChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.SHIPPING_OPTION_ID_REQUIRED, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testSuccessfulChangeShippingAddress() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changeShippingAddress( |
| defaultAddressBundle(), new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mShippingAddressChangeListenerNotified); |
| updateWithDefaultDetails(); |
| verifyUpdatedDefaultDetails(); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangeShippingAddressWithMissingBundle() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changeShippingAddress( |
| null, new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mShippingAddressChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.SHIPPING_ADDRESS_INVALID, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangeShippingAddressWithInvalidCountryCode() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| Bundle invalidAddress = defaultAddressBundle(); |
| invalidAddress.putString(Address.EXTRA_ADDRESS_COUNTRY, ""); |
| mIPaymentDetailsUpdateService.changeShippingAddress( |
| invalidAddress, new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| Assert.assertFalse(mShippingAddressChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.SHIPPING_ADDRESS_INVALID, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testChangeWhileWaitingForPaymentDetailsUpdate() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changePaymentMethod( |
| defaultMethodDataBundle(), new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mMethodChangeListenerNotified); |
| |
| // Call changeShippingOption while waiting for updated payment details. |
| mIPaymentDetailsUpdateService.changeShippingOption( |
| "shipping option id", new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertFalse(mShippingOptionChangeListenerNotified); |
| Assert.assertEquals(ErrorStrings.INVALID_STATE, receivedErrorString()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"Payments"}) |
| public void testPaymentDetailsNotUpdated() throws Throwable { |
| installAndInvokePaymentApp(); |
| startPaymentDetailsUpdateService(); |
| mIPaymentDetailsUpdateService.changeShippingOption( |
| "shipping option id", new PaymentDetailsUpdateServiceCallback()); |
| verifyIsWaitingForPaymentDetailsUpdate(true); |
| Assert.assertTrue(mShippingOptionChangeListenerNotified); |
| onPaymentDetailsNotUpdated(); |
| Assert.assertTrue(mPaymentDetailsDidNotUpdate); |
| verifyIsWaitingForPaymentDetailsUpdate(false); |
| } |
| } |