1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.payments;
6 
7 import android.graphics.Bitmap;
8 import android.graphics.Color;
9 import android.graphics.drawable.BitmapDrawable;
10 
11 import androidx.test.filters.MediumTest;
12 
13 import org.junit.Assert;
14 import org.junit.ClassRule;
15 import org.junit.Rule;
16 import org.junit.Test;
17 import org.junit.runner.RunWith;
18 
19 import org.chromium.base.test.util.CommandLineFlags;
20 import org.chromium.base.test.util.Feature;
21 import org.chromium.chrome.R;
22 import org.chromium.chrome.browser.app.ChromeActivity;
23 import org.chromium.chrome.browser.autofill.AutofillTestHelper;
24 import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile;
25 import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
26 import org.chromium.chrome.browser.flags.ChromeSwitches;
27 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
28 import org.chromium.components.payments.PaymentAppFactoryDelegate;
29 import org.chromium.components.payments.PaymentAppFactoryInterface;
30 import org.chromium.components.payments.PaymentAppService;
31 import org.chromium.components.payments.PaymentFeatureList;
32 import org.chromium.components.payments.SupportedDelegations;
33 import org.chromium.content_public.browser.WebContents;
34 import org.chromium.ui.test.util.DisableAnimationsTestRule;
35 
36 import java.util.concurrent.TimeoutException;
37 
38 /**
39  * A payment integration test for service worker based payment apps.
40  */
41 @RunWith(ChromeJUnit4ClassRunner.class)
42 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
43         // For all the tests in this file, we expect abort exception when there is no supported
44         // payment apps instead of showing payment request UI.
45         "enable-features=" + PaymentFeatureList.STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT + ","
46                 + PaymentFeatureList.SCROLL_TO_EXPAND_PAYMENT_HANDLER,
47         // Prevent crawling the web for real payment apps.
48         "disable-features=" + PaymentFeatureList.SERVICE_WORKER_PAYMENT_APPS})
49 public class PaymentRequestServiceWorkerExpandablePaymentHandlerTest {
50     // Disable animations to reduce flakiness.
51     @ClassRule
52     public static DisableAnimationsTestRule sNoAnimationsRule = new DisableAnimationsTestRule();
53 
54     @Rule
55     public PaymentRequestTestRule mPaymentRequestTestRule = new PaymentRequestTestRule(
56             "payment_request_bobpay_and_basic_card_with_modifier_optional_data_test.html");
57 
58     /**
59      * Installs a mock service worker based payment app with given supported delegations for
60      * testing.
61      *
62      * @param scope                Service worker scope that identifies the payment app. Must be
63      *                             unique.
64      * @param supportedMethodNames The supported payment methods of the mock payment app.
65      * @param name                 The name of the mocked payment app.
66      * @param withIcon             Whether provide payment app icon.
67      * @param supportedDelegations The supported delegations of the mock payment app.
68      */
installMockServiceWorkerPaymentApp(String scope, String[] supportedMethodNames, String name, boolean withIcon, SupportedDelegations supportedDelegations)69     private void installMockServiceWorkerPaymentApp(String scope, String[] supportedMethodNames,
70             String name, boolean withIcon, SupportedDelegations supportedDelegations) {
71         PaymentAppService.getInstance().addFactory(new PaymentAppFactoryInterface() {
72             @Override
73             public void create(PaymentAppFactoryDelegate delegate) {
74                 WebContents webContents = delegate.getParams().getWebContents();
75                 ChromeActivity activity = ChromeActivity.fromWebContents(webContents);
76                 BitmapDrawable icon = withIcon
77                         ? new BitmapDrawable(activity.getResources(),
78                                 Bitmap.createBitmap(new int[] {Color.RED}, 1 /* width */,
79                                         1 /* height */, Bitmap.Config.ARGB_8888))
80                         : null;
81                 delegate.onCanMakePaymentCalculated(true);
82                 delegate.onPaymentAppCreated(new MockPaymentApp(/*identifier=*/scope, name, icon,
83                         supportedMethodNames, supportedDelegations));
84                 delegate.onDoneCreatingPaymentApps(this);
85             }
86         });
87     }
88 
89     /**
90      * Installs a mock service worker based payment app with no supported delegations for testing.
91      *
92      * @param scope                The service worker scope that identifies this payment app. Must
93      *                             be unique.
94      * @param supportedMethodNames The supported payment methods of the mock payment app.
95      * @param withName             Whether provide payment app name.
96      * @param withIcon             Whether provide payment app icon.
97      */
installMockServiceWorkerPaymentApp( String scope, String[] supportedMethodNames, boolean withName, boolean withIcon)98     private void installMockServiceWorkerPaymentApp(
99             String scope, String[] supportedMethodNames, boolean withName, boolean withIcon) {
100         installMockServiceWorkerPaymentApp(scope, supportedMethodNames, withName ? "BobPay" : null,
101                 withIcon, new SupportedDelegations());
102     }
103 
104     /**
105      * Installs a mock service worker based payment app for bobpay with given supported delegations
106      * for testing.
107      *
108      * @param scope             The service worker scope that identifies this payment app. Must be
109      *                          unique.
110      * @param shippingAddress   Whether or not the mock payment app provides shipping address.
111      * @param payerName         Whether or not the mock payment app provides payer's name.
112      * @param payerPhone        Whether or not the mock payment app provides payer's phone number.
113      * @param payerEmail        Whether or not the mock payment app provides payer's email address.
114      * @param name              The name of the mocked payment app.
115      */
installMockServiceWorkerPaymentAppWithDelegations(String scope, boolean shippingAddress, boolean payerName, boolean payerPhone, boolean payerEmail, String name)116     private void installMockServiceWorkerPaymentAppWithDelegations(String scope,
117             boolean shippingAddress, boolean payerName, boolean payerPhone, boolean payerEmail,
118             String name) {
119         String[] supportedMethodNames = {"https://bobpay.xyz"};
120         installMockServiceWorkerPaymentApp(scope, supportedMethodNames, name, true /*withIcon*/,
121                 new SupportedDelegations(shippingAddress, payerName, payerPhone, payerEmail));
122     }
123 
124     /**
125      * Adds a credit cart to ensure that autofill app is available.
126      */
addCreditCard()127     public void addCreditCard() throws TimeoutException {
128         AutofillTestHelper helper = new AutofillTestHelper();
129         String billingAddressId = helper.setProfile(
130                 new AutofillProfile("", "https://example.com", true, "" /* honorific prefix */,
131                         "John Smith", "Google", "340 Main St", "CA", "Los Angeles", "", "90291", "",
132                         "US", "310-310-6000", "john.smith@gmail.com", "en-US"));
133         helper.setCreditCard(new CreditCard("", "https://example.com", true, true, "Jon Doe",
134                 "4111111111111111", "1111", "12", "2050", "visa", R.drawable.visa_card,
135                 billingAddressId, "" /* serverId */));
136     }
137 
138     @Test
139     @MediumTest
140     @Feature({"Payments"})
testNoSupportedPaymentMethods()141     public void testNoSupportedPaymentMethods() throws TimeoutException {
142         mPaymentRequestTestRule.openPageAndClickBuyAndWait(mPaymentRequestTestRule.getShowFailed());
143         mPaymentRequestTestRule.expectResultContains(
144                 new String[] {"show() rejected", "The payment method", "not supported"});
145     }
146 
147     @Test
148     @MediumTest
149     @Feature({"Payments"})
testHasSupportedPaymentMethods()150     public void testHasSupportedPaymentMethods() throws TimeoutException {
151         String[] supportedMethodNames = {"https://bobpay.com"};
152         installMockServiceWorkerPaymentApp("https://bobpay.com", supportedMethodNames, true, true);
153 
154         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
155         // Payment sheet skips to the app since it is the only available app.
156         mPaymentRequestTestRule.openPageAndClickBuyAndWait(mPaymentRequestTestRule.getDismissed());
157     }
158 
159     @Test
160     @MediumTest
161     @Feature({"Payments"})
testDoNotCallCanMakePayment()162     public void testDoNotCallCanMakePayment() throws TimeoutException {
163         // Add a credit card to force showing payment sheet UI.
164         addCreditCard();
165         String[] supportedMethodNames = {"basic-card"};
166         installMockServiceWorkerPaymentApp("https://bobpay.com", supportedMethodNames, true, true);
167 
168         // Sets setCanMakePaymentForTesting(false) to return false for CanMakePayment since there is
169         // no real sw payment app, so if CanMakePayment is called then no payment apps will be
170         // available, otherwise CanMakePayment is not called.
171         PaymentAppServiceBridge.setCanMakePaymentForTesting(false);
172 
173         mPaymentRequestTestRule.triggerUIAndWait(mPaymentRequestTestRule.getReadyForInput());
174         Assert.assertEquals(2, mPaymentRequestTestRule.getNumberOfPaymentApps());
175     }
176 
177     @Test
178     @MediumTest
179     @Feature({"Payments"})
testCanPreselect()180     public void testCanPreselect() throws TimeoutException {
181         String[] supportedMethodNames = {"https://bobpay.com"};
182         installMockServiceWorkerPaymentApp("https://bobpay.com", supportedMethodNames, true, true);
183 
184         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
185 
186         // Payment sheet skips to the app since it is the only available app.
187         mPaymentRequestTestRule.openPageAndClickBuyAndWait(mPaymentRequestTestRule.getDismissed());
188     }
189 
190     @Test
191     @MediumTest
192     @Feature({"Payments"})
testCanNotPreselectWithoutName()193     public void testCanNotPreselectWithoutName() throws TimeoutException {
194         String[] supportedMethodNames = {"https://bobpay.com"};
195         installMockServiceWorkerPaymentApp("https://bobpay.com", supportedMethodNames, false, true);
196 
197         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
198 
199         mPaymentRequestTestRule.triggerUIAndWait(mPaymentRequestTestRule.getReadyForInput());
200         Assert.assertNull(mPaymentRequestTestRule.getSelectedPaymentAppLabel());
201     }
202 
203     @Test
204     @MediumTest
205     @Feature({"Payments"})
testCanNotPreselectWithoutIcon()206     public void testCanNotPreselectWithoutIcon() throws TimeoutException {
207         String[] supportedMethodNames = {"https://bobpay.com"};
208         installMockServiceWorkerPaymentApp("https://bobpay.com", supportedMethodNames, true, false);
209 
210         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
211 
212         mPaymentRequestTestRule.triggerUIAndWait(mPaymentRequestTestRule.getReadyForInput());
213         Assert.assertNull(mPaymentRequestTestRule.getSelectedPaymentAppLabel());
214     }
215 
216     @Test
217     @MediumTest
218     @Feature({"Payments"})
testCanNotPreselectWithoutNameAndIcon()219     public void testCanNotPreselectWithoutNameAndIcon() throws TimeoutException {
220         String[] supportedMethodNames = {"https://bobpay.com"};
221         installMockServiceWorkerPaymentApp(
222                 "https://bobpay.com", supportedMethodNames, false, false);
223 
224         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
225 
226         mPaymentRequestTestRule.triggerUIAndWait(mPaymentRequestTestRule.getReadyForInput());
227         Assert.assertNull(mPaymentRequestTestRule.getSelectedPaymentAppLabel());
228     }
229 
230     @Test
231     @MediumTest
232     @Feature({"Payments"})
testPaymentAppProvidingShippingComesFirst()233     public void testPaymentAppProvidingShippingComesFirst() throws TimeoutException {
234         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
235                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
236                 false /*payerEmail*/, "noSupportedDelegation" /*name*/);
237         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
238                 true /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
239                 false /*payerEmail*/, "shippingSupported1" /*name */);
240         // Install the second app supporting shipping delegation to force showing payment sheet.
241         installMockServiceWorkerPaymentAppWithDelegations("https://charliepay.com" /*scope*/,
242                 true /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
243                 false /*payerEmail*/, "shippingSupported2" /*name */);
244 
245         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
246 
247         mPaymentRequestTestRule.triggerUIAndWait(
248                 "buy_with_shipping_requested", mPaymentRequestTestRule.getReadyForInput());
249         Assert.assertEquals(3, mPaymentRequestTestRule.getNumberOfPaymentApps());
250 
251         // The payment app which provides shipping address must be preselected.
252         Assert.assertTrue(
253                 mPaymentRequestTestRule.getSelectedPaymentAppLabel().contains("shippingSupported"));
254     }
255 
256     @Test
257     @MediumTest
258     @Feature({"Payments"})
testPaymentAppProvidingContactComesFirst()259     public void testPaymentAppProvidingContactComesFirst() throws TimeoutException {
260         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
261                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
262                 false /*payerEmail*/, "noSupportedDelegation" /*name*/);
263         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
264                 false /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
265                 true /*payerEmail*/, "contactSupported" /*name */);
266         installMockServiceWorkerPaymentAppWithDelegations("https://charliepay.com" /*scope*/,
267                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
268                 true /*payerEmail*/, "emailOnlySupported" /*name */);
269         // Install the second app supporting contact delegation to force showing payment sheet.
270         installMockServiceWorkerPaymentAppWithDelegations("https://davepay.com" /*scope*/,
271                 false /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
272                 true /*payerEmail*/, "contactSupported2" /*name */);
273 
274         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
275 
276         mPaymentRequestTestRule.triggerUIAndWait(
277                 "buy_with_contact_requested", mPaymentRequestTestRule.getReadyForInput());
278         Assert.assertEquals(4, mPaymentRequestTestRule.getNumberOfPaymentApps());
279 
280         // The payment app which provides full contact details must be preselected.
281         Assert.assertTrue(
282                 mPaymentRequestTestRule.getSelectedPaymentAppLabel().contains("contactSupported"));
283         // The payment app which partially provides the required contact details comes before the
284         // one that provides no contact information.
285         Assert.assertTrue(mPaymentRequestTestRule.getPaymentMethodSuggestionLabel(2).contains(
286                 "emailOnlySupported"));
287     }
288 
289     @Test
290     @MediumTest
291     @Feature({"Payments"})
testPaymentAppProvidingAllRequiredInfoComesFirst()292     public void testPaymentAppProvidingAllRequiredInfoComesFirst() throws TimeoutException {
293         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
294                 true /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
295                 false /*payerEmail*/, "shippingSupported" /*name */);
296         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
297                 false /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
298                 true /*payerEmail*/, "contactSupported" /*name */);
299         installMockServiceWorkerPaymentAppWithDelegations("https://charliepay.com" /*scope*/,
300                 true /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
301                 true /*payerEmail*/, "shippingAndContactSupported" /*name*/);
302         // Install the second app supporting both shipping and contact delegations to force showing
303         // payment sheet.
304         installMockServiceWorkerPaymentAppWithDelegations("https://davepay.com" /*scope*/,
305                 true /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
306                 true /*payerEmail*/, "shippingAndContactSupported2" /*name*/);
307 
308         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
309 
310         mPaymentRequestTestRule.triggerUIAndWait("buy_with_shipping_and_contact_requested",
311                 mPaymentRequestTestRule.getReadyForInput());
312         Assert.assertEquals(4, mPaymentRequestTestRule.getNumberOfPaymentApps());
313 
314         // The payment app which provides all required information must be preselected.
315         Assert.assertTrue(mPaymentRequestTestRule.getSelectedPaymentAppLabel().contains(
316                 "shippingAndContactSupported"));
317         // The payment app which provides shipping comes before the one which provides contact
318         // details when both required by merchant.
319         Assert.assertTrue(mPaymentRequestTestRule.getPaymentMethodSuggestionLabel(2).contains(
320                 "shippingSupported"));
321     }
322 
323     @Test
324     @MediumTest
325     @Feature({"Payments"})
testSkipsToSinglePaymentAppProvidingShipping()326     public void testSkipsToSinglePaymentAppProvidingShipping() throws TimeoutException {
327         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
328                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
329                 false /*payerEmail*/, "noSupportedDelegation" /*name*/);
330         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
331                 true /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
332                 false /*payerEmail*/, "shippingSupported" /*name */);
333 
334         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
335         mPaymentRequestTestRule.openPageAndClickNodeAndWait(
336                 "buy_with_shipping_requested", mPaymentRequestTestRule.getDismissed());
337     }
338 
339     @Test
340     @MediumTest
341     @Feature({"Payments"})
testSkipsToSinglePaymentAppProvidingContact()342     public void testSkipsToSinglePaymentAppProvidingContact() throws TimeoutException {
343         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
344                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
345                 false /*payerEmail*/, "noSupportedDelegation" /*name*/);
346         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
347                 false /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
348                 true /*payerEmail*/, "contactSupported" /*name */);
349         installMockServiceWorkerPaymentAppWithDelegations("https://charliepay.com" /*scope*/,
350                 false /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
351                 true /*payerEmail*/, "emailOnlySupported" /*name */);
352 
353         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
354         mPaymentRequestTestRule.openPageAndClickNodeAndWait(
355                 "buy_with_contact_requested", mPaymentRequestTestRule.getDismissed());
356     }
357 
358     @Test
359     @MediumTest
360     @Feature({"Payments"})
testSkipsToSinglePaymentAppProvidingAllRequiredInfo()361     public void testSkipsToSinglePaymentAppProvidingAllRequiredInfo() throws TimeoutException {
362         installMockServiceWorkerPaymentAppWithDelegations("https://alicepay.com" /*scope*/,
363                 true /*shippingAddress*/, false /*payerName*/, false /*payerPhone*/,
364                 false /*payerEmail*/, "shippingSupported" /*name */);
365         installMockServiceWorkerPaymentAppWithDelegations("https://bobpay.com" /*scope*/,
366                 false /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
367                 true /*payerEmail*/, "contactSupported" /*name */);
368         installMockServiceWorkerPaymentAppWithDelegations("https://charliepay.com" /*scope*/,
369                 true /*shippingAddress*/, true /*payerName*/, true /*payerPhone*/,
370                 true /*payerEmail*/, "shippingAndContactSupported" /*name*/);
371 
372         PaymentAppServiceBridge.setCanMakePaymentForTesting(true);
373         mPaymentRequestTestRule.openPageAndClickNodeAndWait(
374                 "buy_with_shipping_and_contact_requested", mPaymentRequestTestRule.getDismissed());
375     }
376 }
377