diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationCallback.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationCallback.kt index 67bd91363f..5e07a57b51 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationCallback.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationCallback.kt @@ -1,12 +1,11 @@ package com.braintreepayments.api import androidx.annotation.RestrictTo -import java.lang.Exception /** * @suppress */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -interface AuthorizationCallback { +fun interface AuthorizationCallback { fun onAuthorizationResult(authorization: Authorization?, error: Exception?) } \ No newline at end of file diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.java b/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.java deleted file mode 100644 index 3c74188e69..0000000000 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.braintreepayments.api; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -class AuthorizationLoader { - - private Authorization authorization; - private final ClientTokenProvider clientTokenProvider; - - AuthorizationLoader(@Nullable String initialAuthString, @Nullable ClientTokenProvider clientTokenProvider) { - this.clientTokenProvider = clientTokenProvider; - if (initialAuthString != null) { - this.authorization = Authorization.fromString(initialAuthString); - } - } - - void loadAuthorization(@NonNull final AuthorizationCallback callback) { - if (authorization != null) { - callback.onAuthorizationResult(authorization, null); - } else if (clientTokenProvider != null) { - clientTokenProvider.getClientToken(new ClientTokenCallback() { - @Override - public void onSuccess(@NonNull String clientToken) { - authorization = Authorization.fromString(clientToken); - callback.onAuthorizationResult(authorization, null); - } - - @Override - public void onFailure(@NonNull Exception error) { - callback.onAuthorizationResult(null, error); - } - }); - } else { - String clientSDKSetupURL - = "https://developer.paypal.com/braintree/docs/guides/client-sdk/setup/android/v4#initialization"; - String message = String.format("Authorization required. See %s for more info.", clientSDKSetupURL); - callback.onAuthorizationResult(null, new BraintreeException(message)); - } - } - - @Nullable - Authorization getAuthorizationFromCache() { - return authorization; - } - - void invalidateClientToken() { - if (clientTokenProvider != null) { - authorization = null; - } - } -} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.kt new file mode 100644 index 0000000000..93aa5d6f64 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AuthorizationLoader.kt @@ -0,0 +1,41 @@ +package com.braintreepayments.api + +import androidx.annotation.VisibleForTesting + +internal class AuthorizationLoader( + initialAuthString: String?, + private val clientTokenProvider: ClientTokenProvider? +) { + // cache initial auth if available + @VisibleForTesting + var authorizationFromCache = initialAuthString?.let { Authorization.fromString(it) } + + fun loadAuthorization(callback: AuthorizationCallback) { + if (authorizationFromCache != null) { + callback.onAuthorizationResult(authorizationFromCache, null) + } else if (clientTokenProvider != null) { + clientTokenProvider.getClientToken(object : ClientTokenCallback { + override fun onSuccess(clientToken: String) { + authorizationFromCache = Authorization.fromString(clientToken) + callback.onAuthorizationResult(authorizationFromCache, null) + } + + override fun onFailure(error: Exception) { + callback.onAuthorizationResult(null, error) + } + }) + } else { + val clientSDKSetupURL = + "https://developer.paypal.com/braintree/docs/guides/client-sdk/setup/android/v4#initialization" + val message = "Authorization required. See $clientSDKSetupURL for more info." + callback.onAuthorizationResult(null, BraintreeException(message)) + } + } + + fun invalidateClientToken() { + // only invalidate client token cache if we can fetch a new one with a client token provider + if (clientTokenProvider != null) { + authorizationFromCache = null + } + } +} \ No newline at end of file diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.java b/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.java deleted file mode 100644 index 32b6d1bfc2..0000000000 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.braintreepayments.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class AuthorizationLoaderUnitTest { - - private AuthorizationLoader sut; - - @Test - public void loadAuthorization_whenInitialAuthExists_callsBackAuth() { - String initialAuthString = Fixtures.TOKENIZATION_KEY; - sut = new AuthorizationLoader(initialAuthString, null); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Authorization.class); - verify(callback).onAuthorizationResult(captor.capture(), (Exception) isNull()); - - Authorization authorization = captor.getValue(); - assertEquals(initialAuthString, authorization.toString()); - } - - @Test - public void loadAuthorization_whenInitialAuthExistsAndInvalidateClientTokenCalled_returnsInitialValue() { - String initialAuthString = Fixtures.TOKENIZATION_KEY; - sut = new AuthorizationLoader(initialAuthString, null); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Authorization.class); - verify(callback).onAuthorizationResult(captor.capture(), (Exception) isNull()); - - Authorization authorization = captor.getValue(); - assertEquals(initialAuthString, authorization.toString()); - - sut.invalidateClientToken(); - - AuthorizationCallback callback2 = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback2); - - ArgumentCaptor captor2 = ArgumentCaptor.forClass(Authorization.class); - verify(callback2).onAuthorizationResult(captor2.capture(), (Exception) isNull()); - - Authorization authorization2 = captor2.getValue(); - assertEquals(initialAuthString, authorization2.toString()); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExist_callsBackSuccessfulClientTokenFetch() { - String clientToken = Fixtures.BASE64_CLIENT_TOKEN; - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .clientToken(clientToken) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Authorization.class); - verify(callback).onAuthorizationResult(captor.capture(), (Exception) isNull()); - - Authorization authorization = captor.getValue(); - assertEquals(clientToken, authorization.toString()); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExist_cachesClientTokenInMemory() { - String clientToken = Fixtures.BASE64_CLIENT_TOKEN; - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .clientToken(clientToken) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - sut.loadAuthorization(callback); - - verify(clientTokenProvider, times(1)).getClientToken(any(ClientTokenCallback.class)); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExistAndInvalidateClientTokenCalled_returnsNewClientToken() { - AuthorizationCallback callback = mock(AuthorizationCallback.class); - ArgumentCaptor captor = ArgumentCaptor.forClass(Authorization.class); - - String clientToken1 = Fixtures.BASE64_CLIENT_TOKEN; - String clientToken2 = Fixtures.BASE64_CLIENT_TOKEN2; - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .clientToken(clientToken1, clientToken2) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - sut.loadAuthorization(callback); - verify(callback).onAuthorizationResult(captor.capture(), (Exception) isNull()); - Authorization authorization1 = captor.getValue(); - - sut.invalidateClientToken(); - sut.loadAuthorization(callback); - verify(callback, times(2)).onAuthorizationResult(captor.capture(), (Exception) isNull()); - Authorization authorization2 = captor.getValue(); - - assertNotEquals(authorization1.toString(), authorization2.toString()); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExistAndInvalidateClientTokenCalled_cachesNewClientTokenInMemory() { - String clientToken = Fixtures.BASE64_CLIENT_TOKEN; - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .clientToken(clientToken) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - sut.invalidateClientToken(); - sut.loadAuthorization(callback); - sut.loadAuthorization(callback); - - verify(clientTokenProvider, times(2)).getClientToken(any(ClientTokenCallback.class)); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExist_forwardsClientTokenFetchError() { - Exception clientTokenFetchError = new Exception("error"); - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .error(clientTokenFetchError) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - verify(callback).onAuthorizationResult(null, clientTokenFetchError); - } - - @Test - public void loadAuthorization_whenInitialAuthDoesNotExistAndNoClientTokenProvider_callsBackException() { - sut = new AuthorizationLoader(null, null); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); - verify(callback).onAuthorizationResult((Authorization)isNull(), captor.capture()); - - Exception error = captor.getValue(); - assertTrue(error instanceof BraintreeException); - String expectedMessage = - "Authorization required. See https://developer.paypal.com/braintree/docs/guides/client-sdk/setup/android/v4#initialization for more info."; - assertEquals(expectedMessage, error.getMessage()); - } - - @Test - public void getAuthorizationFromCache_returnsInitialAuthorization() { - String initialAuthString = Fixtures.TOKENIZATION_KEY; - sut = new AuthorizationLoader(initialAuthString, null); - - Authorization cachedAuth = sut.getAuthorizationFromCache(); - assertNotNull(cachedAuth); - assertEquals(initialAuthString, cachedAuth.toString()); - } - - @Test - public void getAuthorizationFromCache_returnsAuthorizationFromClientTokenProvider() { - String clientToken = Fixtures.BASE64_CLIENT_TOKEN; - ClientTokenProvider clientTokenProvider = new MockAuthorizationProviderBuilder() - .clientToken(clientToken) - .build(); - sut = new AuthorizationLoader(null, clientTokenProvider); - - AuthorizationCallback callback = mock(AuthorizationCallback.class); - sut.loadAuthorization(callback); - - Authorization cachedAuth = sut.getAuthorizationFromCache(); - assertNotNull(cachedAuth); - assertEquals(clientToken, cachedAuth.toString()); - } -} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.kt new file mode 100644 index 0000000000..e1199cdd8f --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/AuthorizationLoaderUnitTest.kt @@ -0,0 +1,181 @@ +package com.braintreepayments.api + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AuthorizationLoaderUnitTest { + + private lateinit var sut: AuthorizationLoader + + @Test + fun loadAuthorization_whenInitialAuthExists_callsBackAuth() { + val initialAuthString = Fixtures.TOKENIZATION_KEY + val callback = mockk(relaxed = true) + val authSlot = slot() + every { callback.onAuthorizationResult(capture(authSlot), null) } returns Unit + + sut = AuthorizationLoader(initialAuthString, null) + sut.loadAuthorization(callback) + + val authorization = authSlot.captured + assertEquals(initialAuthString, authorization.toString()) + } + + @Test + fun loadAuthorization_whenInitialAuthExistsAndInvalidateClientTokenCalled_returnsInitialValue() { + val callback1 = mockk(relaxed = true) + val authSlot1 = slot() + every { callback1.onAuthorizationResult(capture(authSlot1), null) } returns Unit + + val initialAuthString = Fixtures.TOKENIZATION_KEY + sut = AuthorizationLoader(initialAuthString, null) + sut.loadAuthorization(callback1) + + val authorization = authSlot1.captured + assertEquals(initialAuthString, authorization.toString()) + + sut.invalidateClientToken() + + val callback2 = mockk() + val authSlot2 = slot() + every { callback2.onAuthorizationResult(capture(authSlot2), null) } returns Unit + + sut.loadAuthorization(callback2) + val authorization2 = authSlot2.captured + assertEquals(initialAuthString, authorization2.toString()) + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExist_callsBackSuccessfulClientTokenFetch() { + val callback = mockk() + val authSlot = slot() + every { callback.onAuthorizationResult(capture(authSlot), null) } returns Unit + + val clientToken = Fixtures.BASE64_CLIENT_TOKEN + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .clientToken(clientToken) + .build() + sut = AuthorizationLoader(null, clientTokenProvider) + + sut.loadAuthorization(callback) + val authorization = authSlot.captured + assertEquals(clientToken, authorization.toString()) + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExist_cachesClientTokenInMemory() { + val clientToken = Fixtures.BASE64_CLIENT_TOKEN + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .clientToken(clientToken) + .build() + sut = AuthorizationLoader(null, clientTokenProvider) + + val callback = mockk(relaxed = true) + sut.loadAuthorization(callback) + sut.loadAuthorization(callback) + + verify(exactly = 1) { clientTokenProvider.getClientToken(any()) } + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExistAndInvalidateClientTokenCalled_returnsNewClientToken() { + val clientToken1 = Fixtures.BASE64_CLIENT_TOKEN + val clientToken2 = Fixtures.BASE64_CLIENT_TOKEN2 + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .clientToken(clientToken1, clientToken2) + .build() + + val callback1 = mockk() + val authSlot1 = slot() + every { callback1.onAuthorizationResult(capture(authSlot1), null) } returns Unit + + sut = AuthorizationLoader(null, clientTokenProvider) + sut.loadAuthorization(callback1) + + val auth1 = authSlot1.captured + + val callback2 = mockk() + val authSlot2 = slot() + every { callback2.onAuthorizationResult(capture(authSlot2), null) } returns Unit + + sut.invalidateClientToken() + sut.loadAuthorization(callback2) + + val auth2 = authSlot2.captured + assertNotEquals(auth1.toString(), auth2.toString()) + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExistAndInvalidateClientTokenCalled_cachesNewClientTokenInMemory() { + val clientToken = Fixtures.BASE64_CLIENT_TOKEN + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .clientToken(clientToken) + .build() + val callback = mockk(relaxed = true) + + sut = AuthorizationLoader(null, clientTokenProvider) + sut.loadAuthorization(callback) + sut.invalidateClientToken() + sut.loadAuthorization(callback) + sut.loadAuthorization(callback) + + verify(exactly = 2) { clientTokenProvider.getClientToken(any()) } + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExist_forwardsClientTokenFetchError() { + val clientTokenFetchError = Exception("error") + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .error(clientTokenFetchError) + .build() + sut = AuthorizationLoader(null, clientTokenProvider) + + val callback = mockk(relaxed = true) + sut.loadAuthorization(callback) + + verify { callback.onAuthorizationResult(null, clientTokenFetchError) } + } + + @Test + fun loadAuthorization_whenInitialAuthDoesNotExistAndNoClientTokenProvider_callsBackException() { + val callback = mockk() + val errorSlot = slot() + every { callback.onAuthorizationResult(null, capture(errorSlot)) } returns Unit + + sut = AuthorizationLoader(null, null) + sut.loadAuthorization(callback) + + val error = errorSlot.captured + val expectedMessage = + "Authorization required. See https://developer.paypal.com/braintree/docs/guides/client-sdk/setup/android/v4#initialization for more info." + assertEquals(expectedMessage, error.message) + } + + @Test + fun authorizationFromCache_returnsInitialAuthorization() { + val initialAuthString = Fixtures.TOKENIZATION_KEY + sut = AuthorizationLoader(initialAuthString, null) + assertEquals(initialAuthString, sut.authorizationFromCache?.toString()) + } + + @Test + fun authorizationFromCache_returnsAuthorizationFromClientTokenProvider() { + val clientToken = Fixtures.BASE64_CLIENT_TOKEN + val clientTokenProvider = MockkAuthorizationProviderBuilder() + .clientToken(clientToken) + .build() + val callback = mockk(relaxed = true) + + sut = AuthorizationLoader(null, clientTokenProvider) + sut.loadAuthorization(callback) + assertEquals(clientToken, sut.authorizationFromCache?.toString()) + } +} \ No newline at end of file diff --git a/TestUtils/src/main/java/com/braintreepayments/api/MockAuthorizationProviderBuilder.java b/TestUtils/src/main/java/com/braintreepayments/api/MockAuthorizationProviderBuilder.java deleted file mode 100644 index cd21b9c33f..0000000000 --- a/TestUtils/src/main/java/com/braintreepayments/api/MockAuthorizationProviderBuilder.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.braintreepayments.api; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class MockAuthorizationProviderBuilder { - - private List clientTokens; - private Exception error; - - public MockAuthorizationProviderBuilder clientToken(String... clientTokens) { - this.clientTokens = new ArrayList<>(Arrays.asList(clientTokens)); - return this; - } - - public MockAuthorizationProviderBuilder error(Exception error) { - this.error = error; - return this; - } - - ClientTokenProvider build() { - ClientTokenProvider clientTokenProvider = mock(ClientTokenProvider.class); - - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - ClientTokenCallback callback = (ClientTokenCallback) invocation.getArguments()[0]; - if (clientTokens != null) { - callback.onSuccess(clientTokens.get(0)); - - // shift array until one item left, at which point all subsequent calls - // will return the last item - if (clientTokens.size() - 1 > 0) { - clientTokens.remove(0); - } - - } else if (error != null) { - callback.onFailure(error); - } - return null; - } - }).when(clientTokenProvider).getClientToken(any(ClientTokenCallback.class)); - - return clientTokenProvider; - } -} diff --git a/TestUtils/src/main/java/com/braintreepayments/api/MockkAuthorizationProviderBuilder.kt b/TestUtils/src/main/java/com/braintreepayments/api/MockkAuthorizationProviderBuilder.kt new file mode 100644 index 0000000000..abc82c204a --- /dev/null +++ b/TestUtils/src/main/java/com/braintreepayments/api/MockkAuthorizationProviderBuilder.kt @@ -0,0 +1,40 @@ +package com.braintreepayments.api + +import io.mockk.every +import io.mockk.mockk + +class MockkAuthorizationProviderBuilder { + + private var error: Exception? = null + private var clientTokens: MutableList? = null + + fun clientToken(vararg clientTokens: String): MockkAuthorizationProviderBuilder { + this.clientTokens = clientTokens.toMutableList() + return this + } + + fun error(error: Exception): MockkAuthorizationProviderBuilder { + this.error = error + return this + } + + fun build(): ClientTokenProvider { + val clientTokenProvider = mockk() + + every { clientTokenProvider.getClientToken(any()) } answers { + // shift array until one item left, at which point all subsequent calls + // will return the last item + val clientToken = + clientTokens?.let { if (it.size > 1) it.removeFirst() else it.first() } + + val callback = firstArg() + clientToken?.let { + callback.onSuccess(it) + } ?: error?.let { + callback.onFailure(it) + } + } + + return clientTokenProvider + } +} \ No newline at end of file