From a74fe2b6ff2c4c00b99d9792d6a817a4353c5c0a Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Tue, 17 Dec 2024 10:07:51 -0600 Subject: [PATCH 1/5] Rename HttpClient.getNumRetriesSoFar to getCurrentRetryCount, convert HttpClientUnitTest to Kotlin, and remove unused function and test class (#1242) --- .../api/sharedutils/ClassHelperTest.java | 27 --- .../api/sharedutils/HttpClient.kt | 7 +- .../api/sharedutils/TLSSocketFactory.java | 14 -- .../api/sharedutils/HttpClientUnitTest.java | 181 ------------------ .../api/sharedutils/HttpClientUnitTest.kt | 174 +++++++++++++++++ 5 files changed, 176 insertions(+), 227 deletions(-) delete mode 100644 SharedUtils/src/androidTest/java/com/braintreepayments/api/sharedutils/ClassHelperTest.java delete mode 100644 SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.java create mode 100644 SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.kt diff --git a/SharedUtils/src/androidTest/java/com/braintreepayments/api/sharedutils/ClassHelperTest.java b/SharedUtils/src/androidTest/java/com/braintreepayments/api/sharedutils/ClassHelperTest.java deleted file mode 100644 index 5544c6f49b..0000000000 --- a/SharedUtils/src/androidTest/java/com/braintreepayments/api/sharedutils/ClassHelperTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.braintreepayments.api.sharedutils; - -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import com.braintreepayments.api.sharedutils.ClassHelper; - -@RunWith(AndroidJUnit4ClassRunner.class) -public class ClassHelperTest { - - @Test - public void isClassAvailable_returnsTrueWhenClassOnClasspath() { - ClassHelper sut = new ClassHelper(); - assertTrue(sut.isClassAvailable("java.lang.String")); - } - - @Test - public void isClassAvailable_returnsFalseWhenClassNotOnClasspath() { - ClassHelper sut = new ClassHelper(); - assertFalse(sut.isClassAvailable("java.lang.NotAClass")); - } -} diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/HttpClient.kt b/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/HttpClient.kt index 169d3e6eca..8d92149625 100644 --- a/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/HttpClient.kt +++ b/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/HttpClient.kt @@ -72,7 +72,7 @@ class HttpClient internal constructor( } if (url != null) { - val retryCount = getNumRetriesSoFar(url) + val retryCount = getCurrentRetryCount(url) val shouldRetry = ((retryCount + 1) < MAX_RETRY_ATTEMPTS) if (shouldRetry) { scheduleRequest(request, retryStrategy, callback) @@ -85,10 +85,7 @@ class HttpClient internal constructor( } } - private fun getNumRetriesSoFar(url: URL): Int { - val retryCount = retryCountMap[url] ?: return 0 - return retryCount - } + private fun getCurrentRetryCount(url: URL) = retryCountMap[url] ?: 0 private fun resetRetryCount(request: HttpRequest) { try { diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/TLSSocketFactory.java b/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/TLSSocketFactory.java index ae71a72709..1c92b98339 100644 --- a/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/TLSSocketFactory.java +++ b/SharedUtils/src/main/java/com/braintreepayments/api/sharedutils/TLSSocketFactory.java @@ -27,20 +27,6 @@ public class TLSSocketFactory extends SSLSocketFactory { private final SSLSocketFactory internalSSLSocketFactory; - static TLSSocketFactory newInstance() throws SSLException { - return new TLSSocketFactory(); - } - - TLSSocketFactory() throws SSLException { - try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); // use system security providers - internalSSLSocketFactory = sslContext.getSocketFactory(); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new SSLException(e.getMessage()); - } - } - /** * @see Android Documentation */ diff --git a/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.java b/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.java deleted file mode 100644 index 2d038b1f2e..0000000000 --- a/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.braintreepayments.api.sharedutils; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class HttpClientUnitTest { - - private SynchronousHttpClient syncHttpClient; - - private HttpRequest httpRequest; - private MockThreadScheduler threadScheduler; - - @Before - public void beforeEach() { - syncHttpClient = mock(SynchronousHttpClient.class); - threadScheduler = spy(new MockThreadScheduler()); - - httpRequest = new HttpRequest().path("https://example.com"); - } - - @Test - public void sendRequest_sendsRequestOnBackgroundThread() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY); - - verifyNoInteractions(syncHttpClient); - threadScheduler.flushBackgroundThread(); - - verify(syncHttpClient).request(httpRequest); - } - - @Test - public void sendRequest_whenBaseHttpClientThrowsException_notifiesErrorViaCallbackOnMainThread() - throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - - Exception exception = new Exception("error"); - when(syncHttpClient.request(httpRequest)).thenThrow(exception); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY); - - threadScheduler.flushBackgroundThread(); - verify(callback, never()).onResult(null, exception); - - threadScheduler.flushMainThread(); - verify(callback).onResult(null, exception); - } - - @Test - public void sendRequest_onBaseHttpClientRequestSuccess_notifiesSuccessViaCallbackOnMainThread() - throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - - when(syncHttpClient.request(httpRequest)).thenReturn(response); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY); - - threadScheduler.flushBackgroundThread(); - verify(callback, never()).onResult(response, null); - - threadScheduler.flushMainThread(); - verify(callback).onResult(response, null); - } - - @Test - public void sendRequest_whenCallbackIsNull_doesNotNotifySuccess() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - - when(syncHttpClient.request(httpRequest)).thenReturn(response); - sut.sendRequest(httpRequest, null, HttpClient.RetryStrategy.NO_RETRY); - - threadScheduler.flushBackgroundThread(); - verify(threadScheduler, never()).runOnMain(any(Runnable.class)); - } - - @Test - public void sendRequest_whenCallbackIsNull_doesNotNotifyError() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - - Exception exception = new Exception("error"); - when(syncHttpClient.request(httpRequest)).thenThrow(exception); - - sut.sendRequest(httpRequest, null, HttpClient.RetryStrategy.NO_RETRY); - - threadScheduler.flushBackgroundThread(); - verify(threadScheduler, never()).runOnMain(any(Runnable.class)); - } - - @Test - public void sendRequest_whenRetryMax3TimesEnabled_retriesRequest3Times() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - - Exception exception = new Exception("error"); - when(syncHttpClient.request(httpRequest)).thenThrow(exception); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES); - - threadScheduler.flushBackgroundThread(); - verify(syncHttpClient, times(3)).request(httpRequest); - } - - @Test - public void sendRequest_whenRetryMax3TimesEnabled_notifiesMaxRetriesLimitExceededOnForegroundThread() - throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - - Exception exception = new Exception("error"); - when(syncHttpClient.request(httpRequest)).thenThrow(exception); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES); - - threadScheduler.flushBackgroundThread(); - verify(callback, never()).onResult((HttpResponse) isNull(), any(Exception.class)); - - threadScheduler.flushMainThread(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); - verify(callback).onResult((HttpResponse) isNull(), captor.capture()); - - HttpClientException httpClientException = (HttpClientException) captor.getValue(); - String expectedMessage = "Retry limit has been exceeded. Try again later."; - assertEquals(expectedMessage, httpClientException.getMessage()); - } - - @Test - public void sendRequest_whenRetryMax3TimesEnabled_futureRequestsAreAllowed() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - - Exception exception = new Exception("error"); - when(syncHttpClient.request(httpRequest)).thenThrow(exception); - - NetworkResponseCallback callback = mock(NetworkResponseCallback.class); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES); - - threadScheduler.flushBackgroundThread(); - - reset(syncHttpClient); - when(syncHttpClient.request(httpRequest)) - .thenThrow(exception) - .thenReturn(response); - sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES); - - threadScheduler.flushBackgroundThread(); - threadScheduler.flushMainThread(); - - verify(callback).onResult(response, null); - } - - @Test - public void sendRequestSynchronous_sendsHttpRequest() throws Exception { - HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - - when(syncHttpClient.request(httpRequest)).thenReturn(response); - - String result = sut.sendRequest(httpRequest); - assertEquals("response body", result); - } -} diff --git a/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.kt b/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.kt new file mode 100644 index 0000000000..f7ebff5f64 --- /dev/null +++ b/SharedUtils/src/test/java/com/braintreepayments/api/sharedutils/HttpClientUnitTest.kt @@ -0,0 +1,174 @@ +package com.braintreepayments.api.sharedutils + +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mockito + +class HttpClientUnitTest { + private lateinit var syncHttpClient: SynchronousHttpClient + private lateinit var httpRequest: HttpRequest + private lateinit var threadScheduler: MockThreadScheduler + + private lateinit var sut: HttpClient + + @Before + fun beforeEach() { + syncHttpClient = Mockito.mock(SynchronousHttpClient::class.java) + threadScheduler = Mockito.spy(MockThreadScheduler()) + httpRequest = HttpRequest().path("https://example.com") + + sut = HttpClient(syncHttpClient, threadScheduler) + } + + @Test + @Throws(Exception::class) + fun sendRequest_sendsRequestOnBackgroundThread() { + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY) + + Mockito.verifyNoInteractions(syncHttpClient) + threadScheduler.flushBackgroundThread() + + Mockito.verify(syncHttpClient)?.request(httpRequest) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenBaseHttpClientThrowsException_notifiesErrorViaCallbackOnMainThread() { + val exception = Exception("error") + Mockito.`when`(syncHttpClient.request(httpRequest)).thenThrow(exception) + + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY) + + threadScheduler.flushBackgroundThread() + Mockito.verify(callback, Mockito.never()).onResult(null, exception) + + threadScheduler.flushMainThread() + Mockito.verify(callback).onResult(null, exception) + } + + @Test + @Throws(Exception::class) + fun sendRequest_onBaseHttpClientRequestSuccess_notifiesSuccessViaCallbackOnMainThread() { + val response = HttpResponse("response body", HttpResponseTiming(123, 456)) + + Mockito.`when`(syncHttpClient.request(httpRequest)).thenReturn(response) + + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.NO_RETRY) + + threadScheduler.flushBackgroundThread() + Mockito.verify(callback, Mockito.never()).onResult(response, null) + + threadScheduler.flushMainThread() + Mockito.verify(callback).onResult(response, null) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenCallbackIsNull_doesNotNotifySuccess() { + val response = HttpResponse("response body", HttpResponseTiming(123, 456)) + + Mockito.`when`(syncHttpClient.request(httpRequest)).thenReturn(response) + sut.sendRequest(httpRequest, null, HttpClient.RetryStrategy.NO_RETRY) + + threadScheduler.flushBackgroundThread() + Mockito.verify(threadScheduler, Mockito.never())?.runOnMain( + ArgumentMatchers.any(Runnable::class.java) + ) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenCallbackIsNull_doesNotNotifyError() { + val exception = Exception("error") + Mockito.`when`(syncHttpClient.request(httpRequest)).thenThrow(exception) + + sut.sendRequest(httpRequest, null, HttpClient.RetryStrategy.NO_RETRY) + + threadScheduler.flushBackgroundThread() + Mockito.verify(threadScheduler, Mockito.never())?.runOnMain( + ArgumentMatchers.any( + Runnable::class.java + ) + ) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenRetryMax3TimesEnabled_retriesRequest3Times() { + val exception = Exception("error") + Mockito.`when`(syncHttpClient.request(httpRequest)).thenThrow(exception) + + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES) + + threadScheduler.flushBackgroundThread() + Mockito.verify(syncHttpClient, Mockito.times(3))?.request(httpRequest) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenRetryMax3TimesEnabled_notifiesMaxRetriesLimitExceededOnForegroundThread() { + val exception = Exception("error") + Mockito.`when`(syncHttpClient.request(httpRequest)).thenThrow(exception) + + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES) + + threadScheduler.flushBackgroundThread() + Mockito.verify(callback, Mockito.never()).onResult( + ArgumentMatchers.isNull(), + ArgumentMatchers.any(Exception::class.java) + ) + + threadScheduler.flushMainThread() + + val captor = ArgumentCaptor.forClass(Exception::class.java) + Mockito.verify(callback).onResult(ArgumentMatchers.isNull(), captor.capture()) + + val httpClientException = captor.value as HttpClientException + val expectedMessage = "Retry limit has been exceeded. Try again later." + Assert.assertEquals(expectedMessage, httpClientException.message) + } + + @Test + @Throws(Exception::class) + fun sendRequest_whenRetryMax3TimesEnabled_futureRequestsAreAllowed() { + val response = HttpResponse("response body", HttpResponseTiming(123, 456)) + + val exception = Exception("error") + Mockito.`when`(syncHttpClient.request(httpRequest)).thenThrow(exception) + + val callback = Mockito.mock(NetworkResponseCallback::class.java) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES) + + threadScheduler.flushBackgroundThread() + + Mockito.reset(syncHttpClient) + Mockito.`when`(syncHttpClient.request(httpRequest)) + .thenThrow(exception) + .thenReturn(response) + sut.sendRequest(httpRequest, callback, HttpClient.RetryStrategy.RETRY_MAX_3_TIMES) + + threadScheduler.flushBackgroundThread() + threadScheduler.flushMainThread() + + Mockito.verify(callback).onResult(response, null) + } + + @Test + @Throws(Exception::class) + fun sendRequestSynchronous_sendsHttpRequest() { + val response = HttpResponse("response body", HttpResponseTiming(123, 456)) + + Mockito.`when`(syncHttpClient.request(httpRequest)).thenReturn(response) + + val result = sut.sendRequest(httpRequest) + Assert.assertEquals("response body", result) + } +} From 4bfcf658cab25446d1c968ea2ae5b6f735089722 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Tue, 17 Dec 2024 10:08:16 -0600 Subject: [PATCH 2/5] Fix Shopper Insights PayPal flow and PayPal Vault flow (#1245) --- .../com/braintreepayments/demo/PayPalRequestFactory.java | 1 + .../com/braintreepayments/demo/ShopperInsightsFragment.kt | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java b/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java index efdc0d8bd5..3a5902b708 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java +++ b/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java @@ -61,6 +61,7 @@ public static PayPalVaultRequest createPayPalVaultRequest( postalAddress.setStreetAddress("123 Fake Street"); postalAddress.setExtendedAddress("Floor A"); postalAddress.setLocality("San Francisco"); + postalAddress.setPostalCode("12345"); postalAddress.setRegion("CA"); postalAddress.setCountryCodeAlpha2("US"); diff --git a/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt b/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt index 65ad45b48e..28739b3d57 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt +++ b/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt @@ -69,8 +69,9 @@ class ShopperInsightsFragment : BaseFragment() { venmoClient = VenmoClient(requireContext(), super.getAuthStringArg(), null) payPalClient = PayPalClient( - requireContext(), super.getAuthStringArg(), - Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/") + requireContext(), + super.getAuthStringArg(), + Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"), ) return inflater.inflate(R.layout.fragment_shopping_insights, container, false) From 2447b2f18bd0a67415469084f803aa94f2ea0044 Mon Sep 17 00:00:00 2001 From: Tim Chow Date: Wed, 18 Dec 2024 09:20:07 -0600 Subject: [PATCH 3/5] Update all AnalyticsEventParams properties to vals (#1243) --- .../api/core/AnalyticsEventParams.kt | 12 +-- .../api/localpayment/LocalPaymentClient.kt | 5 +- .../LocalPaymentClientUnitTest.java | 5 +- .../api/paypal/PayPalClientUnitTest.java | 90 +++++++++++-------- .../api/venmo/VenmoClient.kt | 10 ++- .../api/venmo/VenmoClientUnitTest.java | 8 +- 6 files changed, 77 insertions(+), 53 deletions(-) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt index fa309fb55b..fbebf15c20 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt @@ -20,12 +20,12 @@ import androidx.annotation.RestrictTo */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class AnalyticsEventParams @JvmOverloads constructor( - var payPalContextId: String? = null, - var linkType: String? = null, - var isVaultRequest: Boolean = false, - var startTime: Long? = null, - var endTime: Long? = null, - var endpoint: String? = null, + val payPalContextId: String? = null, + val linkType: String? = null, + val isVaultRequest: Boolean = false, + val startTime: Long? = null, + val endTime: Long? = null, + val endpoint: String? = null, val experiment: String? = null, val paymentMethodsDisplayed: List = emptyList(), val appSwitchUrl: String? = null diff --git a/LocalPayment/src/main/java/com/braintreepayments/api/localpayment/LocalPaymentClient.kt b/LocalPayment/src/main/java/com/braintreepayments/api/localpayment/LocalPaymentClient.kt index 71f91792b0..f1cbf98d85 100644 --- a/LocalPayment/src/main/java/com/braintreepayments/api/localpayment/LocalPaymentClient.kt +++ b/LocalPayment/src/main/java/com/braintreepayments/api/localpayment/LocalPaymentClient.kt @@ -213,8 +213,9 @@ class LocalPaymentClient internal constructor( } private fun sendAnalyticsEvent(eventName: String) { - val eventParameters = AnalyticsEventParams() - eventParameters.payPalContextId = payPalContextId + val eventParameters = AnalyticsEventParams( + payPalContextId = payPalContextId + ) braintreeClient.sendAnalyticsEvent(eventName, eventParameters) } diff --git a/LocalPayment/src/test/java/com/braintreepayments/api/localpayment/LocalPaymentClientUnitTest.java b/LocalPayment/src/test/java/com/braintreepayments/api/localpayment/LocalPaymentClientUnitTest.java index 22f8b84217..360a64feba 100644 --- a/LocalPayment/src/test/java/com/braintreepayments/api/localpayment/LocalPaymentClientUnitTest.java +++ b/LocalPayment/src/test/java/com/braintreepayments/api/localpayment/LocalPaymentClientUnitTest.java @@ -375,8 +375,9 @@ public void createPaymentAuthRequest_success_withPaymentId_sendsAnalyticsEvents( sut.createPaymentAuthRequest(getIdealLocalPaymentRequest(), localPaymentAuthCallback); verify(braintreeClient).sendAnalyticsEvent(LocalPaymentAnalytics.PAYMENT_STARTED, new AnalyticsEventParams()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setPayPalContextId("some-paypal-context-id"); + AnalyticsEventParams params = new AnalyticsEventParams( + "some-paypal-context-id" + ); verify(braintreeClient).sendAnalyticsEvent(LocalPaymentAnalytics.BROWSER_SWITCH_SUCCEEDED, params); } diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java index 07f0418064..5f084a0072 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java @@ -284,8 +284,11 @@ public void createPaymentAuthRequest_whenPayPalNotEnabled_returnsError() { "for more information.", ((PayPalPaymentAuthRequest.Failure) request).getError().getMessage()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setVaultRequest(false); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + null, + false + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, params); } @@ -309,8 +312,11 @@ public void createPaymentAuthRequest_whenCheckoutRequest_whenConfigError_forward assertTrue(request instanceof PayPalPaymentAuthRequest.Failure); assertEquals(authError, ((PayPalPaymentAuthRequest.Failure) request).getError()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setVaultRequest(false); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + null, + false + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, params); } @@ -334,8 +340,11 @@ public void requestBillingAgreement_whenConfigError_forwardsErrorToListener() { assertTrue(request instanceof PayPalPaymentAuthRequest.Failure); assertEquals(authError, ((PayPalPaymentAuthRequest.Failure) request).getError()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setVaultRequest(true); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + null, + true + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, params); } @@ -414,9 +423,11 @@ public void createPaymentAuthRequest_whenVaultRequest_sendsAppSwitchStartedEvent assertFalse(browserSwitchOptions.isLaunchAsNewTask()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setVaultRequest(true); - params.setLinkType("universal"); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + "universal", + true + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_STARTED, params); } @@ -541,8 +552,11 @@ public void tokenize_whenCancelUriReceived_notifiesCancellationAndSendsAnalytics PayPalResult result = captor.getValue(); assertTrue(result instanceof PayPalResult.Cancel); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setVaultRequest(false); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + null, + false + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.BROWSER_LOGIN_CANCELED, params); } @@ -582,9 +596,11 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_callsBackResult() assertTrue(result instanceof PayPalResult.Success); assertEquals(payPalAccountNonce, ((PayPalResult.Success) result).getNonce()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setPayPalContextId("EC-HERMES-SANDBOX-EC-TOKEN"); - params.setVaultRequest(false); + AnalyticsEventParams params = new AnalyticsEventParams( + "EC-HERMES-SANDBOX-EC-TOKEN", + null, + false + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_SUCCEEDED, params); } @@ -624,19 +640,20 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_sendsAppSwitchSuccee assertTrue(result instanceof PayPalResult.Success); assertEquals(payPalAccountNonce, ((PayPalResult.Success) result).getNonce()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setPayPalContextId("EC-HERMES-SANDBOX-EC-TOKEN"); + AnalyticsEventParams params = new AnalyticsEventParams( + "EC-HERMES-SANDBOX-EC-TOKEN" + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_SUCCEEDED, params); AnalyticsEventParams appSwitchParams = new AnalyticsEventParams( - "EC-HERMES-SANDBOX-EC-TOKEN", - null, - false, - null, - null, - null, - null, - emptyList(), - "sample-scheme://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN&switch_initiated_time=17166111926211" + "EC-HERMES-SANDBOX-EC-TOKEN", + null, + false, + null, + null, + null, + null, + emptyList(), + "sample-scheme://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN&switch_initiated_time=17166111926211" ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_SUCCEEDED, appSwitchParams); } @@ -666,19 +683,20 @@ public void tokenize_whenPayPalNotEnabled_sendsAppSwitchFailedEvents() throws JS sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setPayPalContextId("SOME-BA"); + AnalyticsEventParams params = new AnalyticsEventParams( + "SOME-BA" + ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.TOKENIZATION_FAILED, params); AnalyticsEventParams appSwitchParams = new AnalyticsEventParams( - "SOME-BA", - null, - false, - null, - null, - null, - null, - emptyList(), - "https://some-scheme/onetouch/v1/cancel?token=SOME-BA&switch_initiated_time=17166111926211" + "SOME-BA", + null, + false, + null, + null, + null, + null, + emptyList(), + "https://some-scheme/onetouch/v1/cancel?token=SOME-BA&switch_initiated_time=17166111926211" ); verify(braintreeClient).sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_FAILED, appSwitchParams); } diff --git a/Venmo/src/main/java/com/braintreepayments/api/venmo/VenmoClient.kt b/Venmo/src/main/java/com/braintreepayments/api/venmo/VenmoClient.kt index efaef70666..754186192d 100644 --- a/Venmo/src/main/java/com/braintreepayments/api/venmo/VenmoClient.kt +++ b/Venmo/src/main/java/com/braintreepayments/api/venmo/VenmoClient.kt @@ -387,10 +387,12 @@ class VenmoClient internal constructor( private val analyticsParams: AnalyticsEventParams get() { - val eventParameters = AnalyticsEventParams(appSwitchUrl = venmoRepository.venmoUrl.toString()) - eventParameters.payPalContextId = payPalContextId - eventParameters.linkType = LINK_TYPE - eventParameters.isVaultRequest = isVaultRequest + val eventParameters = AnalyticsEventParams( + payPalContextId = payPalContextId, + linkType = LINK_TYPE, + isVaultRequest = isVaultRequest, + appSwitchUrl = venmoRepository.venmoUrl.toString(), + ) return eventParameters } diff --git a/Venmo/src/test/java/com/braintreepayments/api/venmo/VenmoClientUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/venmo/VenmoClientUnitTest.java index b0d0925f73..6807891f72 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/venmo/VenmoClientUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/venmo/VenmoClientUnitTest.java @@ -960,9 +960,11 @@ public void tokenize_withFailedVaultCall_forwardsErrorToActivityResultListener_a assertTrue(result instanceof VenmoResult.Failure); assertEquals(error, ((VenmoResult.Failure) result).getError()); - AnalyticsEventParams params = new AnalyticsEventParams(); - params.setLinkType(LINK_TYPE); - params.setVaultRequest(true); + AnalyticsEventParams params = new AnalyticsEventParams( + null, + LINK_TYPE, + true + ); verify(braintreeClient).sendAnalyticsEvent(VenmoAnalytics.TOKENIZE_FAILED, expectedVaultAnalyticsParams); } From 017b5d44cf78ac0176370a7bc65a7f3d702f2122 Mon Sep 17 00:00:00 2001 From: Rich Herrera Date: Mon, 13 Jan 2025 14:12:51 -0600 Subject: [PATCH 4/5] Add Payer Email to Base Parameters if not null/empty (#1254) * Add isNullOrEmpty validation to add payer_email when it exists * Suppress CyclomaticComplexMethod * Update CHANGELOG --- CHANGELOG.md | 5 +++++ .../com/braintreepayments/api/paypal/PayPalVaultRequest.kt | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4199684611..c2c035ee1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Braintree Android SDK Release Notes +## unreleased + +* PayPal + * Fix bug to ensure that `PayPalVaultRequest.userAuthenticationEmail` is not sent as an empty string + ## 5.3.0 (2024-12-11) * PayPal diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt index ea19bfb618..973e605589 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt @@ -66,7 +66,7 @@ class PayPalVaultRequest ) { @Throws(JSONException::class) - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod") override fun createRequestBody( configuration: Configuration?, authorization: Authorization?, @@ -90,7 +90,9 @@ class PayPalVaultRequest parameters.put(DESCRIPTION_KEY, billingAgreementDescription) } - parameters.putOpt(PAYER_EMAIL_KEY, userAuthenticationEmail) + if (!userAuthenticationEmail.isNullOrEmpty()) { + parameters.put(PAYER_EMAIL_KEY, userAuthenticationEmail) + } userPhoneNumber?.let { parameters.put(PHONE_NUMBER_KEY, it.toJson()) } From 45cbb6f0613c91d470d605c24530ed1d5be426e7 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Tue, 14 Jan 2025 13:47:59 -0600 Subject: [PATCH 5/5] Bug Fix: Handle Missing 3DS `dfReferenceId` (#1255) * update logic to handle missing dfReferenceId * add unit test for empty dfReferenceId; remove now inaccurate test * add CHANGELOG entry --- CHANGELOG.md | 2 + .../api/threedsecure/ThreeDSecureClient.kt | 14 ++-- .../ThreeDSecureClientUnitTest.java | 66 ++++++++----------- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c035ee1c..2dd56e16c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * PayPal * Fix bug to ensure that `PayPalVaultRequest.userAuthenticationEmail` is not sent as an empty string +* ThreeDSecure + * Return error if no `dfReferenceId` is returned in the 3D Secure flow ## 5.3.0 (2024-12-11) diff --git a/ThreeDSecure/src/main/java/com/braintreepayments/api/threedsecure/ThreeDSecureClient.kt b/ThreeDSecure/src/main/java/com/braintreepayments/api/threedsecure/ThreeDSecureClient.kt index 121487ecf4..f84437bb5d 100644 --- a/ThreeDSecure/src/main/java/com/braintreepayments/api/threedsecure/ThreeDSecureClient.kt +++ b/ThreeDSecure/src/main/java/com/braintreepayments/api/threedsecure/ThreeDSecureClient.kt @@ -204,11 +204,17 @@ class ThreeDSecureClient internal constructor( configuration, request ) { consumerSessionId: String?, _ -> - if (consumerSessionId != null) { - try { + if (!consumerSessionId.isNullOrEmpty()) { lookupJSON.put("dfReferenceId", consumerSessionId) - } catch (ignored: JSONException) { - } + } else { + callbackPrepareLookupFailure( + callback, + ThreeDSecurePrepareLookupResult.Failure( + BraintreeException("There was an error retrieving the dfReferenceId.") + ) + ) + + return@initialize } braintreeClient.sendAnalyticsEvent(ThreeDSecureAnalytics.LOOKUP_SUCCEEDED) callback.onPrepareLookupResult( diff --git a/ThreeDSecure/src/test/java/com/braintreepayments/api/threedsecure/ThreeDSecureClientUnitTest.java b/ThreeDSecure/src/test/java/com/braintreepayments/api/threedsecure/ThreeDSecureClientUnitTest.java index 8db8436e20..8377b5adcd 100644 --- a/ThreeDSecure/src/test/java/com/braintreepayments/api/threedsecure/ThreeDSecureClientUnitTest.java +++ b/ThreeDSecure/src/test/java/com/braintreepayments/api/threedsecure/ThreeDSecureClientUnitTest.java @@ -128,10 +128,9 @@ public void prepareLookup_returnsValidLookupJSONString() } @Test - public void prepareLookup_returnsValidLookupJSONString_whenCardinalSetupFails() - throws JSONException, BraintreeException { + public void prepareLookup_initializesCardinal() throws BraintreeException { CardinalClient cardinalClient = new MockCardinalClientBuilder() - .error(new Exception("cardinal error")) + .successReferenceId("fake-df") .build(); BraintreeClient braintreeClient = new MockBraintreeClientBuilder() @@ -148,32 +147,16 @@ public void prepareLookup_returnsValidLookupJSONString_whenCardinalSetupFails() ThreeDSecurePrepareLookupCallback callback = mock(ThreeDSecurePrepareLookupCallback.class); sut.prepareLookup(activity, basicRequest, callback); - ArgumentCaptor captor = ArgumentCaptor.forClass(ThreeDSecurePrepareLookupResult.class); - verify(callback).onPrepareLookupResult(captor.capture()); - - ThreeDSecurePrepareLookupResult prepareLookupResult = captor.getValue(); - assertTrue(prepareLookupResult instanceof ThreeDSecurePrepareLookupResult.Success); - assertSame(basicRequest, ((ThreeDSecurePrepareLookupResult.Success) prepareLookupResult).getRequest()); - - String clientData = ((ThreeDSecurePrepareLookupResult.Success) prepareLookupResult).getClientData(); - JSONObject lookup = new JSONObject(clientData); - Assert.assertEquals("encoded_auth_fingerprint", - lookup.getString("authorizationFingerprint")); - Assert.assertEquals(lookup.getString("braintreeLibraryVersion"), - "Android-" + com.braintreepayments.api.core.BuildConfig.VERSION_NAME); - Assert.assertEquals(lookup.getString("nonce"), "a-nonce"); - assertFalse(lookup.has("dfReferenceId")); - - JSONObject clientMetaData = lookup.getJSONObject("clientMetadata"); - Assert.assertEquals(clientMetaData.getString("requestedThreeDSecureVersion"), "2"); - Assert.assertEquals(clientMetaData.getString("sdkVersion"), - "Android/" + com.braintreepayments.api.core.BuildConfig.VERSION_NAME); + verify(cardinalClient).initialize(same(activity), same(threeDSecureEnabledConfig), + same(basicRequest), any(CardinalInitializeCallback.class)); } @Test - public void prepareLookup_initializesCardinal() throws BraintreeException { + public void prepareLookup_whenCardinalClientInitializeFails_forwardsError() + throws BraintreeException { + BraintreeException initializeRuntimeError = new BraintreeException("initialize error"); CardinalClient cardinalClient = new MockCardinalClientBuilder() - .successReferenceId("fake-df") + .initializeRuntimeError(initializeRuntimeError) .build(); BraintreeClient braintreeClient = new MockBraintreeClientBuilder() @@ -190,27 +173,28 @@ public void prepareLookup_initializesCardinal() throws BraintreeException { ThreeDSecurePrepareLookupCallback callback = mock(ThreeDSecurePrepareLookupCallback.class); sut.prepareLookup(activity, basicRequest, callback); - verify(cardinalClient).initialize(same(activity), same(threeDSecureEnabledConfig), - same(basicRequest), any(CardinalInitializeCallback.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(ThreeDSecurePrepareLookupResult.class); + verify(callback).onPrepareLookupResult(captor.capture()); + ThreeDSecurePrepareLookupResult prepareLookupResult = captor.getValue(); + assertTrue(prepareLookupResult instanceof ThreeDSecurePrepareLookupResult.Failure); + assertEquals(initializeRuntimeError, ((ThreeDSecurePrepareLookupResult.Failure) prepareLookupResult).getError()); } @Test - public void prepareLookup_whenCardinalClientInitializeFails_forwardsError() - throws BraintreeException { - BraintreeException initializeRuntimeError = new BraintreeException("initialize error"); + public void prepareLookup_whenDfReferenceIdMissing_forwardsError() throws BraintreeException { CardinalClient cardinalClient = new MockCardinalClientBuilder() - .initializeRuntimeError(initializeRuntimeError) - .build(); + .successReferenceId("") + .build(); BraintreeClient braintreeClient = new MockBraintreeClientBuilder() - .configuration(threeDSecureEnabledConfig) - .build(); + .configuration(threeDSecureEnabledConfig) + .build(); ThreeDSecureClient sut = new ThreeDSecureClient( - braintreeClient, - cardinalClient, - new ThreeDSecureAPI(braintreeClient), - merchantRepository + braintreeClient, + cardinalClient, + new ThreeDSecureAPI(braintreeClient), + merchantRepository ); ThreeDSecurePrepareLookupCallback callback = mock(ThreeDSecurePrepareLookupCallback.class); @@ -220,7 +204,11 @@ public void prepareLookup_whenCardinalClientInitializeFails_forwardsError() verify(callback).onPrepareLookupResult(captor.capture()); ThreeDSecurePrepareLookupResult prepareLookupResult = captor.getValue(); assertTrue(prepareLookupResult instanceof ThreeDSecurePrepareLookupResult.Failure); - assertEquals(initializeRuntimeError, ((ThreeDSecurePrepareLookupResult.Failure) prepareLookupResult).getError()); + Exception error = ((ThreeDSecurePrepareLookupResult.Failure) prepareLookupResult).getError(); + + TestCase.assertTrue(error instanceof BraintreeException); + Assert.assertEquals(error.getMessage(), + "There was an error retrieving the dfReferenceId."); } @Test