From a61c12b272cca388a9ceabbcd519d8b0bbcc2575 Mon Sep 17 00:00:00 2001 From: Chintan Soni <114917119+chintan-soni-cko@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:46:00 +0100 Subject: [PATCH] PIMOB:2138 - CVV Tokenization were implemented (#243) --- .../com/checkout/CheckoutApiServiceFactory.kt | 18 ++- .../com/checkout/api/CheckoutApiClient.kt | 3 + .../com/checkout/api/CheckoutApiService.kt | 12 ++ .../checkout/tokenization/NetworkApiClient.kt | 13 ++ .../tokenization/TokenNetworkApiClient.kt | 23 ++++ .../tokenization/entity/TokenDataEntity.kt | 10 ++ .../request/CVVToTokenNetworkRequestMapper.kt | 18 +++ .../CVVTokenizationNetworkDataMapper.kt | 26 ++++ .../tokenization/model/CVVTokenDetails.kt | 5 + .../tokenization/model/CVVTokenRequest.kt | 12 -- .../model/CVVTokenizationRequest.kt | 18 +++ .../model/CVVTokenizationResultHandler.kt | 22 ++++ .../model/ValidateCVVTokenizationRequest.kt | 8 ++ .../repository/TokenRepository.kt | 3 + .../repository/TokenRepositoryImpl.kt | 53 +++++++- .../request/CVVTokenNetworkRequest.kt | 14 ++ .../response/CVVTokenDetailsResponse.kt | 21 +++ .../ValidateCVVTokenizationDataUseCase.kt | 24 ++++ .../utils/TokenizationConstants.kt | 1 + .../validator/CVVComponentDetailsValidator.kt | 14 +- .../com/checkout/api/CheckoutApiClientTest.kt | 13 ++ .../mock/GetTokenDetailsResponseTestJson.kt | 18 +++ ...Data.kt => TokenizationRequestTestData.kt} | 6 +- .../tokenization/TokenNetworkApiClientTest.kt | 108 ++++++++++++++- .../logging/TokenizationEventLoggerTest.kt | 10 +- .../CardTokenizationNetworkDataMapperTest.kt | 10 +- .../AddressToAddressEntityDataMapperTest.kt | 4 +- .../CVVToTokenNetworkRequestMapperTest.kt | 34 +++++ .../request/CardToTokenRequestMapperTest.kt | 8 +- .../PhoneToPhoneEntityDataMapperTest.kt | 4 +- .../AddressEntityToAddressDataMapperTest.kt | 4 +- .../CVVTokenizationNetworkDataMapperTest.kt | 124 ++++++++++++++++++ .../PhoneEntityToPhoneDataMapperTest.kt | 4 +- .../repository/TokenRepositoryImplTest.kt | 123 +++++++++++++++-- .../CVVComponentDetailsValidatorTest.kt | 26 +++- .../ValidateCVVTokenizationDataUseCaseTest.kt | 96 ++++++++++++++ .../ValidateTokenizationDataUseCaseTest.kt | 19 +-- .../styling/CustomCVVInputFieldStyle.kt | 6 +- .../frames/ui/component/CustomButton.kt | 15 ++- .../frames/ui/component/LoadCVVComponent.kt | 49 +++++++ .../frames/ui/component/ThreedsComponent.kt | 17 +-- .../frames/ui/extension/showAlertDialog.kt | 15 +++ .../frames/ui/screen/CVVTokenizationScreen.kt | 62 ++++++++- .../ui/screen/LoadCVVComponentsContents.kt | 38 +++--- .../example/frames/ui/utils/Constants.kt | 5 + .../ui/viewmodel/CVVTokenizationViewModel.kt | 1 + .../frames/cvvinputfield/CVVInputFieldTest.kt | 10 +- ... => InternalCVVComponentMediatorUITest.kt} | 13 +- .../frames/cvvinputfield/CVVInputField.kt | 4 +- .../cvvinputfield/api/CVVComponentMediator.kt | 10 +- .../api/InternalCVVComponentApi.kt | 9 +- .../api/InternalCVVComponentMediator.kt | 31 +++-- .../models/InternalCVVTokenRequest.kt | 10 ++ .../usecase/CVVTokenizationUseCase.kt | 15 +++ .../viewmodel/CVVInputFieldViewModel.kt | 9 +- .../InternalCVVComponentApiTest.kt | 26 ++-- .../InternalCVVComponentMediatorTest.kt | 51 +++++++ .../usecase/CVVTokenizationUseCaseTest.kt | 51 +++++++ .../viewmodel/CVVInputFieldViewModelTest.kt | 2 +- 59 files changed, 1221 insertions(+), 157 deletions(-) create mode 100644 checkout/src/main/java/com/checkout/tokenization/entity/TokenDataEntity.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapper.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapper.kt delete mode 100644 checkout/src/main/java/com/checkout/tokenization/model/CVVTokenRequest.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationRequest.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationResultHandler.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/model/ValidateCVVTokenizationRequest.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/request/CVVTokenNetworkRequest.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/response/CVVTokenDetailsResponse.kt create mode 100644 checkout/src/main/java/com/checkout/tokenization/usecase/ValidateCVVTokenizationDataUseCase.kt rename checkout/src/test/java/com/checkout/mock/{CardTokenTestData.kt => TokenizationRequestTestData.kt} (93%) create mode 100644 checkout/src/test/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapperTest.kt create mode 100644 checkout/src/test/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapperTest.kt create mode 100644 checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateCVVTokenizationDataUseCaseTest.kt create mode 100644 example_app_frames/src/main/java/com/checkout/example/frames/ui/component/LoadCVVComponent.kt create mode 100644 example_app_frames/src/main/java/com/checkout/example/frames/ui/extension/showAlertDialog.kt rename frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/{InternalCVVComponentMediatorTest.kt => InternalCVVComponentMediatorUITest.kt} (80%) create mode 100644 frames/src/main/java/com/checkout/frames/cvvinputfield/models/InternalCVVTokenRequest.kt create mode 100644 frames/src/main/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCase.kt create mode 100644 frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt create mode 100644 frames/src/test/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCaseTest.kt diff --git a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt index 9208873aa..5f064e9c0 100644 --- a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt +++ b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt @@ -16,10 +16,13 @@ import com.checkout.threedsecure.usecase.ProcessThreeDSUseCase import com.checkout.tokenization.TokenNetworkApiClient import com.checkout.tokenization.logging.TokenizationEventLogger import com.checkout.tokenization.mapper.request.AddressToAddressValidationRequestDataMapper +import com.checkout.tokenization.mapper.request.CVVToTokenNetworkRequestMapper import com.checkout.tokenization.mapper.request.CardToTokenRequestMapper +import com.checkout.tokenization.mapper.response.CVVTokenizationNetworkDataMapper import com.checkout.tokenization.mapper.response.CardTokenizationNetworkDataMapper import com.checkout.tokenization.repository.TokenRepository import com.checkout.tokenization.repository.TokenRepositoryImpl +import com.checkout.tokenization.usecase.ValidateCVVTokenizationDataUseCase import com.checkout.tokenization.usecase.ValidateTokenizationDataUseCase import com.checkout.validation.validator.AddressValidator import com.checkout.validation.validator.PhoneValidator @@ -48,17 +51,20 @@ public object CheckoutApiServiceFactory { publicKey: String, environment: Environment ): TokenRepository = TokenRepositoryImpl( - provideNetworkApiClient(publicKey, environment.url), - CardToTokenRequestMapper(), - CardTokenizationNetworkDataMapper(), - ValidateTokenizationDataUseCase( + networkApiClient = provideNetworkApiClient(publicKey, environment.url), + cardToTokenRequestMapper = CardToTokenRequestMapper(), + cvvToTokenNetworkRequestMapper = CVVToTokenNetworkRequestMapper(), + cardTokenizationNetworkDataMapper = CardTokenizationNetworkDataMapper(), + validateTokenizationDataUseCase = ValidateTokenizationDataUseCase( CardValidatorFactory.createInternal(), AddressValidator(), PhoneValidator(), AddressToAddressValidationRequestDataMapper() ), - TokenizationEventLogger(EventLoggerProvider.provide()), - publicKey + validateCVVTokenizationDataUseCase = ValidateCVVTokenizationDataUseCase(CVVComponentValidatorFactory.create()), + logger = TokenizationEventLogger(EventLoggerProvider.provide()), + publicKey = publicKey, + cvvTokenizationNetworkDataMapper = CVVTokenizationNetworkDataMapper() ) private fun provideNetworkApiClient( diff --git a/checkout/src/main/java/com/checkout/api/CheckoutApiClient.kt b/checkout/src/main/java/com/checkout/api/CheckoutApiClient.kt index 2a569b3dc..a8e7d1afc 100644 --- a/checkout/src/main/java/com/checkout/api/CheckoutApiClient.kt +++ b/checkout/src/main/java/com/checkout/api/CheckoutApiClient.kt @@ -5,6 +5,7 @@ import com.checkout.logging.EventLoggerProvider import com.checkout.logging.model.LoggingEvent import com.checkout.threedsecure.Executor import com.checkout.threedsecure.model.ThreeDSRequest +import com.checkout.tokenization.model.CVVTokenizationRequest import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.GooglePayTokenRequest import com.checkout.tokenization.repository.TokenRepository @@ -23,5 +24,7 @@ internal class CheckoutApiClient( override fun createToken(request: GooglePayTokenRequest) = tokenRepository.sendGooglePayTokenRequest(request) + override fun createToken(request: CVVTokenizationRequest) = tokenRepository.sendCVVTokenizationRequest(request) + override fun handleThreeDS(request: ThreeDSRequest) = threeDSExecutor.execute(request) } diff --git a/checkout/src/main/java/com/checkout/api/CheckoutApiService.kt b/checkout/src/main/java/com/checkout/api/CheckoutApiService.kt index dbb58791d..bd0472345 100644 --- a/checkout/src/main/java/com/checkout/api/CheckoutApiService.kt +++ b/checkout/src/main/java/com/checkout/api/CheckoutApiService.kt @@ -1,7 +1,9 @@ package com.checkout.api +import androidx.annotation.RestrictTo import androidx.annotation.UiThread import com.checkout.threedsecure.model.ThreeDSRequest +import com.checkout.tokenization.model.CVVTokenizationRequest import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.GooglePayTokenRequest @@ -26,6 +28,16 @@ public interface CheckoutApiService { @UiThread public fun createToken(request: GooglePayTokenRequest) + /** + * Creates a CVV token based on the provided [CVVTokenizationRequest]. + * @param request - [CVVTokenizationRequest] contains CVV details and result handlers. + * + * Note - This method is restrict to use for internal library group purpose only + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @UiThread + public fun createToken(request: CVVTokenizationRequest) + /** * Handle 3DS process with WebView. * diff --git a/checkout/src/main/java/com/checkout/tokenization/NetworkApiClient.kt b/checkout/src/main/java/com/checkout/tokenization/NetworkApiClient.kt index 384b2c971..907d8548f 100644 --- a/checkout/src/main/java/com/checkout/tokenization/NetworkApiClient.kt +++ b/checkout/src/main/java/com/checkout/tokenization/NetworkApiClient.kt @@ -1,8 +1,10 @@ package com.checkout.tokenization import com.checkout.network.response.NetworkApiResponse +import com.checkout.tokenization.request.CVVTokenNetworkRequest import com.checkout.tokenization.request.GooglePayTokenNetworkRequest import com.checkout.tokenization.request.TokenRequest +import com.checkout.tokenization.response.CVVTokenDetailsResponse import com.checkout.tokenization.response.TokenDetailsResponse /** @@ -17,6 +19,17 @@ internal interface NetworkApiClient { */ suspend fun sendCardTokenRequest(cardTokenRequest: TokenRequest): NetworkApiResponse + /** + * Sends a CVV tokenization request to the network. + * + * @param cvvTokenNetworkRequest The network request containing the CVV tokenization details. + * + * @return A [NetworkApiResponse] representing the response from the network. + * It encapsulates the result as a [CVVTokenDetailsResponse] on success or an error message on failure. + */ + suspend fun sendCVVTokenRequest(cvvTokenNetworkRequest: CVVTokenNetworkRequest): + NetworkApiResponse + /** Sending GooglePayToken request * * @param googlePayTokenNetworkRequest: the request to execute diff --git a/checkout/src/main/java/com/checkout/tokenization/TokenNetworkApiClient.kt b/checkout/src/main/java/com/checkout/tokenization/TokenNetworkApiClient.kt index 750d38f31..73d582841 100644 --- a/checkout/src/main/java/com/checkout/tokenization/TokenNetworkApiClient.kt +++ b/checkout/src/main/java/com/checkout/tokenization/TokenNetworkApiClient.kt @@ -3,8 +3,10 @@ package com.checkout.tokenization import com.checkout.network.extension.executeHttpRequest import com.checkout.network.response.ErrorResponse import com.checkout.network.response.NetworkApiResponse +import com.checkout.tokenization.request.CVVTokenNetworkRequest import com.checkout.tokenization.request.GooglePayTokenNetworkRequest import com.checkout.tokenization.request.TokenRequest +import com.checkout.tokenization.response.CVVTokenDetailsResponse import com.checkout.tokenization.response.TokenDetailsResponse import com.checkout.tokenization.utils.TokenizationConstants import com.squareup.moshi.Moshi @@ -42,6 +44,27 @@ internal class TokenNetworkApiClient( ) } + override suspend fun sendCVVTokenRequest( + cvvTokenNetworkRequest: CVVTokenNetworkRequest + ): NetworkApiResponse { + + val jsonTokenRequestAdapter = moshiClient.adapter(CVVTokenNetworkRequest::class.java) + + val requestBody = + jsonTokenRequestAdapter.toJson(cvvTokenNetworkRequest).toRequestBody(TokenizationConstants.jsonMediaType) + + val request = Request.Builder() + .url(url) + .post(requestBody) + .build() + + return okHttpClient.executeHttpRequest( + request, + moshiClient.adapter(CVVTokenDetailsResponse::class.java), + moshiClient.adapter(ErrorResponse::class.java) + ) + } + override suspend fun sendGooglePayTokenRequest( googlePayTokenNetworkRequest: GooglePayTokenNetworkRequest ): NetworkApiResponse { diff --git a/checkout/src/main/java/com/checkout/tokenization/entity/TokenDataEntity.kt b/checkout/src/main/java/com/checkout/tokenization/entity/TokenDataEntity.kt new file mode 100644 index 000000000..bca097cc9 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/entity/TokenDataEntity.kt @@ -0,0 +1,10 @@ +package com.checkout.tokenization.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TokenDataEntity internal constructor( + @Json(name = "cvv") + val cvv: String = "" +) diff --git a/checkout/src/main/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapper.kt b/checkout/src/main/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapper.kt new file mode 100644 index 000000000..41c90ee65 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapper.kt @@ -0,0 +1,18 @@ +package com.checkout.tokenization.mapper.request + +import com.checkout.base.mapper.Mapper +import com.checkout.tokenization.entity.TokenDataEntity +import com.checkout.tokenization.model.CVVTokenizationRequest +import com.checkout.tokenization.request.CVVTokenNetworkRequest +import com.checkout.tokenization.utils.TokenizationConstants + +/** + * Mapping of [CVVTokenizationRequest] to [CVVTokenNetworkRequest] + */ +internal class CVVToTokenNetworkRequestMapper : Mapper { + + override fun map(from: CVVTokenizationRequest): CVVTokenNetworkRequest = CVVTokenNetworkRequest( + TokenizationConstants.CVV, + TokenDataEntity(from.cvv) + ) +} diff --git a/checkout/src/main/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapper.kt b/checkout/src/main/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapper.kt new file mode 100644 index 000000000..ed4da569f --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapper.kt @@ -0,0 +1,26 @@ +package com.checkout.tokenization.mapper.response + +import com.checkout.base.error.CheckoutError +import com.checkout.tokenization.mapper.TokenizationNetworkDataMapper +import com.checkout.tokenization.error.TokenizationError.Companion.TOKENIZATION_API_MALFORMED_JSON +import com.checkout.tokenization.model.CVVTokenDetails +import com.checkout.tokenization.response.GetCVVTokenDetailsResponse + +/** + * An implementation of mapping from [GetCVVTokenDetailsResponse] data object to [CVVTokenDetails]. + */ +internal class CVVTokenizationNetworkDataMapper : TokenizationNetworkDataMapper() { + + override fun createMappedResult(resultBody: T): CVVTokenDetails = when (resultBody) { + is GetCVVTokenDetailsResponse -> fromCardTokenizationResponse(resultBody) + else -> throw CheckoutError( + TOKENIZATION_API_MALFORMED_JSON, "${resultBody.javaClass.name} cannot be mapped to a CVVTokenDetails" + ) + } + + private fun fromCardTokenizationResponse(result: GetCVVTokenDetailsResponse) = with(result) { + CVVTokenDetails( + type = type, token = token, expiresOn = expiresOn + ) + } +} diff --git a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenDetails.kt b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenDetails.kt index 75cd57e9b..c1133a3d4 100644 --- a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenDetails.kt +++ b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenDetails.kt @@ -4,6 +4,11 @@ package com.checkout.tokenization.model * A representation of a [CVVTokenDetails] contains tokenization response */ public data class CVVTokenDetails( + /** + * The type of tokenization + */ + val type: String, + /** * The reference token */ diff --git a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenRequest.kt b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenRequest.kt deleted file mode 100644 index 566c299bf..000000000 --- a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.checkout.tokenization.model - -/** - * Used for Creating a CVV token request - * - * @param onSuccess invokes when token api returns success response holding the [CVVTokenDetails] - * @param onFailure invokes when token api returns failure response holding the errorMessage - */ -public data class CVVTokenRequest( - val onSuccess: (tokenDetails: CVVTokenDetails) -> Unit, - val onFailure: (errorMessage: String) -> Unit -) diff --git a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationRequest.kt b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationRequest.kt new file mode 100644 index 000000000..dacaeccef --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationRequest.kt @@ -0,0 +1,18 @@ +package com.checkout.tokenization.model + +import com.checkout.base.model.CardScheme + +/** + * Represents a request to tokenize a CVV. + * + * @param cvv The CVV value to tokenize. + * @param cardScheme (Optional) The card scheme for the CVV to validate, if known. Default is [CardScheme.UNKNOWN]. + * @param resultHandler A callback function to handle the tokenization result. It will receive a + * [CVVTokenizationResultHandler] which can be used to handle success or failure outcomes. + * + */ +public data class CVVTokenizationRequest( + val cvv: String, + val cardScheme: CardScheme = CardScheme.UNKNOWN, + val resultHandler: (CVVTokenizationResultHandler) -> Unit, +) diff --git a/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationResultHandler.kt b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationResultHandler.kt new file mode 100644 index 000000000..568855fd7 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/model/CVVTokenizationResultHandler.kt @@ -0,0 +1,22 @@ +package com.checkout.tokenization.model + +/** + * Sealed class representing the result of a CVV tokenization operation. + * It can either be a success containing [Success] or a failure containing [Failure]. + */ +public sealed class CVVTokenizationResultHandler { + + /** + * Represents a successful CVV tokenization result. + * + * @property tokenDetails [CVVTokenDetails] The details of the generated CVV token. + */ + public data class Success(val tokenDetails: CVVTokenDetails) : CVVTokenizationResultHandler() + + /** + * Represents a failed CVV tokenization result. + * + * @property errorMessage A description of the error that occurred during tokenization. + */ + public data class Failure(val errorMessage: String) : CVVTokenizationResultHandler() +} diff --git a/checkout/src/main/java/com/checkout/tokenization/model/ValidateCVVTokenizationRequest.kt b/checkout/src/main/java/com/checkout/tokenization/model/ValidateCVVTokenizationRequest.kt new file mode 100644 index 000000000..e663b1c26 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/model/ValidateCVVTokenizationRequest.kt @@ -0,0 +1,8 @@ +package com.checkout.tokenization.model + +import com.checkout.base.model.CardScheme + +internal data class ValidateCVVTokenizationRequest( + val cvv: String, + val cardScheme: CardScheme = CardScheme.UNKNOWN +) diff --git a/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepository.kt b/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepository.kt index c615481aa..f1ddef0fd 100644 --- a/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepository.kt +++ b/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepository.kt @@ -1,5 +1,6 @@ package com.checkout.tokenization.repository +import com.checkout.tokenization.model.CVVTokenizationRequest import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.GooglePayTokenRequest @@ -7,5 +8,7 @@ internal interface TokenRepository { fun sendCardTokenRequest(cardTokenRequest: CardTokenRequest) + fun sendCVVTokenizationRequest(cvvTokenizationRequest: CVVTokenizationRequest) + fun sendGooglePayTokenRequest(googlePayTokenRequest: GooglePayTokenRequest) } diff --git a/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepositoryImpl.kt b/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepositoryImpl.kt index 4b8da6dd7..8530d27ed 100644 --- a/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepositoryImpl.kt +++ b/checkout/src/main/java/com/checkout/tokenization/repository/TokenRepositoryImpl.kt @@ -10,13 +10,19 @@ import com.checkout.tokenization.entity.GooglePayEntity import com.checkout.tokenization.error.TokenizationError import com.checkout.tokenization.logging.TokenizationLogger import com.checkout.tokenization.mapper.TokenizationNetworkDataMapper +import com.checkout.tokenization.model.CVVTokenDetails +import com.checkout.tokenization.model.CVVTokenizationRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler import com.checkout.tokenization.model.GooglePayTokenRequest import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.TokenDetails import com.checkout.tokenization.model.TokenResult import com.checkout.tokenization.model.Card +import com.checkout.tokenization.model.ValidateCVVTokenizationRequest +import com.checkout.tokenization.request.CVVTokenNetworkRequest import com.checkout.tokenization.request.GooglePayTokenNetworkRequest import com.checkout.tokenization.request.TokenRequest +import com.checkout.tokenization.response.CVVTokenDetailsResponse import com.checkout.tokenization.response.TokenDetailsResponse import com.checkout.tokenization.utils.TokenizationConstants import com.checkout.validation.model.ValidationResult @@ -28,13 +34,17 @@ import kotlinx.coroutines.CoroutineScope import org.json.JSONException import org.json.JSONObject +@Suppress("LongParameterList") internal class TokenRepositoryImpl( private val networkApiClient: NetworkApiClient, private val cardToTokenRequestMapper: Mapper, + private val cvvToTokenNetworkRequestMapper: Mapper, private val cardTokenizationNetworkDataMapper: TokenizationNetworkDataMapper, private val validateTokenizationDataUseCase: UseCase>, + private val validateCVVTokenizationDataUseCase: UseCase>, private val logger: TokenizationLogger, - private val publicKey: String + private val publicKey: String, + private val cvvTokenizationNetworkDataMapper: TokenizationNetworkDataMapper, ) : TokenRepository { @VisibleForTesting @@ -76,6 +86,47 @@ internal class TokenRepositoryImpl( } } + @Suppress("TooGenericExceptionCaught") + override fun sendCVVTokenizationRequest(cvvTokenizationRequest: CVVTokenizationRequest) { + with(cvvTokenizationRequest) { + networkCoroutineScope.launch { + val validateCVVRequest = ValidateCVVTokenizationRequest( + cvv = cvv, cardScheme = cardScheme + ) + + val validationTokenizationDataResult = validateCVVTokenizationDataUseCase.execute(validateCVVRequest) + + val response: NetworkApiResponse = when (validationTokenizationDataResult) { + is ValidationResult.Failure -> { + NetworkApiResponse.InternalError(validationTokenizationDataResult.error) + } + + is ValidationResult.Success -> { + networkApiClient.sendCVVTokenRequest( + cvvToTokenNetworkRequestMapper.map(from = cvvTokenizationRequest) + ) + } + } + + val tokenResult = cvvTokenizationNetworkDataMapper.toTokenResult(response) + + launch(Dispatchers.Main) { + when (tokenResult) { + is TokenResult.Success -> { + resultHandler(CVVTokenizationResultHandler.Success(tokenResult.result)) + } + + is TokenResult.Failure -> { + tokenResult.error.message?.let { + resultHandler(CVVTokenizationResultHandler.Failure(it)) + } + } + } + } + } + } + } + @Suppress("TooGenericExceptionCaught") override fun sendGooglePayTokenRequest(googlePayTokenRequest: GooglePayTokenRequest) { var response: NetworkApiResponse diff --git a/checkout/src/main/java/com/checkout/tokenization/request/CVVTokenNetworkRequest.kt b/checkout/src/main/java/com/checkout/tokenization/request/CVVTokenNetworkRequest.kt new file mode 100644 index 000000000..b569d3191 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/request/CVVTokenNetworkRequest.kt @@ -0,0 +1,14 @@ +package com.checkout.tokenization.request + +import com.checkout.tokenization.entity.TokenDataEntity +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class CVVTokenNetworkRequest( + @Json(name = "type") + val type: String, + + @Json(name = "token_data") + val tokenDataEntity: TokenDataEntity +) diff --git a/checkout/src/main/java/com/checkout/tokenization/response/CVVTokenDetailsResponse.kt b/checkout/src/main/java/com/checkout/tokenization/response/CVVTokenDetailsResponse.kt new file mode 100644 index 000000000..7558f4166 --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/response/CVVTokenDetailsResponse.kt @@ -0,0 +1,21 @@ +package com.checkout.tokenization.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Token details returns as the response to CVVTokenization call. + */ +internal typealias GetCVVTokenDetailsResponse = CVVTokenDetailsResponse + +@JsonClass(generateAdapter = true) +internal data class CVVTokenDetailsResponse( + @Json(name = "type") + val type: String, + + @Json(name = "token") + val token: String, + + @Json(name = "expires_on") + val expiresOn: String, +) diff --git a/checkout/src/main/java/com/checkout/tokenization/usecase/ValidateCVVTokenizationDataUseCase.kt b/checkout/src/main/java/com/checkout/tokenization/usecase/ValidateCVVTokenizationDataUseCase.kt new file mode 100644 index 000000000..4a79f7aaf --- /dev/null +++ b/checkout/src/main/java/com/checkout/tokenization/usecase/ValidateCVVTokenizationDataUseCase.kt @@ -0,0 +1,24 @@ +package com.checkout.tokenization.usecase + +import com.checkout.base.usecase.UseCase +import com.checkout.tokenization.model.ValidateCVVTokenizationRequest +import com.checkout.validation.api.CVVComponentValidator +import com.checkout.validation.error.ValidationError +import com.checkout.validation.model.ValidationResult + +internal class ValidateCVVTokenizationDataUseCase( + private val cvvComponentValidator: CVVComponentValidator, +) : UseCase> { + + override fun execute(data: ValidateCVVTokenizationRequest): ValidationResult { + with(cvvComponentValidator.validate(data.cvv, data.cardScheme)) { + if (this is ValidationResult.Failure || data.cvv.isEmpty()) return ValidationResult.Failure( + ValidationError( + errorCode = ValidationError.CVV_INVALID_LENGTH, message = "Please enter a valid security code" + ) + ) + } + + return ValidationResult.Success(Unit) + } +} diff --git a/checkout/src/main/java/com/checkout/tokenization/utils/TokenizationConstants.kt b/checkout/src/main/java/com/checkout/tokenization/utils/TokenizationConstants.kt index 568ed9109..ecb2a3360 100644 --- a/checkout/src/main/java/com/checkout/tokenization/utils/TokenizationConstants.kt +++ b/checkout/src/main/java/com/checkout/tokenization/utils/TokenizationConstants.kt @@ -7,5 +7,6 @@ internal object TokenizationConstants { // tokenization types const val CARD = "card" + const val CVV = "cvv" const val GOOGLE_PAY = "googlepay" } diff --git a/checkout/src/main/java/com/checkout/validation/validator/CVVComponentDetailsValidator.kt b/checkout/src/main/java/com/checkout/validation/validator/CVVComponentDetailsValidator.kt index 6bc6f2fe9..63e5c19f1 100644 --- a/checkout/src/main/java/com/checkout/validation/validator/CVVComponentDetailsValidator.kt +++ b/checkout/src/main/java/com/checkout/validation/validator/CVVComponentDetailsValidator.kt @@ -2,13 +2,21 @@ package com.checkout.validation.validator import com.checkout.base.model.CardScheme import com.checkout.validation.api.CVVComponentValidator +import com.checkout.validation.error.ValidationError import com.checkout.validation.model.CvvValidationRequest +import com.checkout.validation.model.ValidationResult import com.checkout.validation.validator.contract.Validator internal class CVVComponentDetailsValidator(private val cvvValidator: Validator) : CVVComponentValidator { - override fun validate(cvv: String, cardScheme: CardScheme) = cvvValidator.validate( - CvvValidationRequest(cvv, cardScheme) - ) + override fun validate(cvv: String, cardScheme: CardScheme): ValidationResult = if (cvv.isEmpty()) { + ValidationResult.Failure( + ValidationError( + errorCode = ValidationError.CVV_INVALID_LENGTH, message = "Please enter a valid security code" + ) + ) + } else { + cvvValidator.validate(CvvValidationRequest(cvv, cardScheme)) + } } diff --git a/checkout/src/test/java/com/checkout/api/CheckoutApiClientTest.kt b/checkout/src/test/java/com/checkout/api/CheckoutApiClientTest.kt index 9ab943465..e5d9d40e3 100644 --- a/checkout/src/test/java/com/checkout/api/CheckoutApiClientTest.kt +++ b/checkout/src/test/java/com/checkout/api/CheckoutApiClientTest.kt @@ -7,6 +7,7 @@ import com.checkout.logging.Logger import com.checkout.logging.model.LoggingEvent import com.checkout.threedsecure.Executor import com.checkout.threedsecure.model.ThreeDSRequest +import com.checkout.tokenization.model.CVVTokenizationRequest import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.GooglePayTokenRequest import com.checkout.tokenization.repository.TokenRepository @@ -74,6 +75,18 @@ internal class CheckoutApiClientTest { verify { mockTokenRepository.sendCardTokenRequest(eq(mockRequest)) } } + @Test + fun `when create token requested then send cvv token request with correct data is invoked`() { + // Given + val mockRequest = mockk() + + // When + checkoutApiService.createToken(mockRequest) + + // Then + verify { mockTokenRepository.sendCVVTokenizationRequest(eq(mockRequest)) } + } + @Test fun `when create token requested then send google pay token request with correct data is invoked`() { // Given diff --git a/checkout/src/test/java/com/checkout/mock/GetTokenDetailsResponseTestJson.kt b/checkout/src/test/java/com/checkout/mock/GetTokenDetailsResponseTestJson.kt index 3846133d6..97149602a 100644 --- a/checkout/src/test/java/com/checkout/mock/GetTokenDetailsResponseTestJson.kt +++ b/checkout/src/test/java/com/checkout/mock/GetTokenDetailsResponseTestJson.kt @@ -47,6 +47,14 @@ internal object GetTokenDetailsResponseTestJson { } """.trimIndent() + val cvvTokenDetailsResponse = """ + { + "type": "cvv", + "token": "tok_ubfj2q76miwundwlk72vxt2i7q", + "expires_on": "2019-08-24T14:15:22Z" + } + """.trimIndent() + val googlePayTokenDetailsResponse = """ { "type": "googlepay", @@ -77,6 +85,16 @@ internal object GetTokenDetailsResponseTestJson { } """.trimIndent() + val cvvTokenDetailsErrorResponse = """ + { + "request_id": "745f1863-e67f-451d-ae3e-cb2050fc4640", + "error_type": "request_invalid", + "error_codes": [ + "cvv_invalid" + ] + } + """.trimIndent() + val googlePayTokenDetailsErrorResponse = """ { "request_id":"e9945f8e-c69d-4ad6-9961-49a62c5f2a7e", diff --git a/checkout/src/test/java/com/checkout/mock/CardTokenTestData.kt b/checkout/src/test/java/com/checkout/mock/TokenizationRequestTestData.kt similarity index 93% rename from checkout/src/test/java/com/checkout/mock/CardTokenTestData.kt rename to checkout/src/test/java/com/checkout/mock/TokenizationRequestTestData.kt index 091402141..3d50438ad 100644 --- a/checkout/src/test/java/com/checkout/mock/CardTokenTestData.kt +++ b/checkout/src/test/java/com/checkout/mock/TokenizationRequestTestData.kt @@ -1,17 +1,19 @@ package com.checkout.mock +import com.checkout.base.model.CardScheme import com.checkout.base.model.Country import com.checkout.network.response.ErrorResponse import com.checkout.tokenization.entity.AddressEntity import com.checkout.tokenization.entity.PhoneEntity import com.checkout.tokenization.model.Address +import com.checkout.tokenization.model.CVVTokenizationRequest import com.checkout.tokenization.model.Phone import com.checkout.tokenization.model.ExpiryDate import com.checkout.tokenization.model.Card import com.checkout.tokenization.response.TokenDetailsResponse import com.checkout.validation.model.AddressValidationRequest -internal object CardTokenTestData { +internal object TokenizationRequestTestData { val addressEntity = AddressEntity( "Checkout.com", @@ -43,6 +45,8 @@ internal object CardTokenTestData { country ) + val cvvTokenizationRequest = CVVTokenizationRequest(cvv = "123", cardScheme = CardScheme.VISA, resultHandler = { }) + val phoneEntity = PhoneEntity("4155552671", "44") val phone = Phone("4155552671", country) diff --git a/checkout/src/test/java/com/checkout/tokenization/TokenNetworkApiClientTest.kt b/checkout/src/test/java/com/checkout/tokenization/TokenNetworkApiClientTest.kt index f0132c424..a79d32516 100644 --- a/checkout/src/test/java/com/checkout/tokenization/TokenNetworkApiClientTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/TokenNetworkApiClientTest.kt @@ -2,11 +2,12 @@ package com.checkout.tokenization import android.os.Build import androidx.annotation.RequiresApi -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.mock.GetTokenDetailsResponseTestJson import com.checkout.network.OkHttpProvider import com.checkout.network.response.NetworkApiResponse import com.checkout.tokenization.entity.GooglePayEntity +import com.checkout.tokenization.mapper.request.CVVToTokenNetworkRequestMapper import com.checkout.tokenization.mapper.request.CardToTokenRequestMapper import com.checkout.tokenization.request.GooglePayTokenNetworkRequest import com.checkout.tokenization.utils.TokenizationConstants @@ -95,7 +96,7 @@ internal class TokenNetworkApiClientTest { // When val response = tokenNetworkApiClient.sendCardTokenRequest( - CardToTokenRequestMapper().map(CardTokenTestData.card) + CardToTokenRequestMapper().map(TokenizationRequestTestData.card) ) launch { @@ -116,8 +117,8 @@ internal class TokenNetworkApiClientTest { issuerCountry.shouldBeEqualTo("US") productId.shouldBeEqualTo("F") productType.shouldBeEqualTo("CLASSIC") - billingAddress.shouldBeEqualTo(CardTokenTestData.addressEntity) - phone.shouldBeEqualTo(CardTokenTestData.phoneEntity) + billingAddress.shouldBeEqualTo(TokenizationRequestTestData.addressEntity) + phone.shouldBeEqualTo(TokenizationRequestTestData.phoneEntity) name.shouldBeEqualTo("Bruce Wayne") } @@ -141,7 +142,7 @@ internal class TokenNetworkApiClientTest { // When val response = tokenNetworkApiClient.sendCardTokenRequest( - CardToTokenRequestMapper().map(CardTokenTestData.card) + CardToTokenRequestMapper().map(TokenizationRequestTestData.card) ) // Then @@ -171,7 +172,102 @@ internal class TokenNetworkApiClientTest { // When val response = tokenNetworkApiClient.sendCardTokenRequest( - CardToTokenRequestMapper().map(CardTokenTestData.card) + CardToTokenRequestMapper().map(TokenizationRequestTestData.card) + ) + + launch { + // Then + assertFalse(response is NetworkApiResponse.Success) + + assertFalse(response is NetworkApiResponse.ServerError) + + assertTrue(response is NetworkApiResponse.NetworkError) + with((response as NetworkApiResponse.NetworkError)) { + throwable.shouldNotBeNull() + } + + assertFalse(response is NetworkApiResponse.InternalError) + } + } + } + + @DisplayName("Get CVVToken Details") + @Nested + inner class GetCVVTokenDetails { + @DisplayName("cvv token details: The information is extracted from the response") + @Test + fun informationExtractedFromResponse() = runTest { + // Given + enqueueMockResponse( + 200, + GetTokenDetailsResponseTestJson.cvvTokenDetailsResponse + ) + + // When + val response = tokenNetworkApiClient.sendCVVTokenRequest( + CVVToTokenNetworkRequestMapper().map(TokenizationRequestTestData.cvvTokenizationRequest) + ) + + launch { + // Then + assertTrue(response is NetworkApiResponse.Success) + with((response as NetworkApiResponse.Success).body) { + type.shouldBeEqualTo("cvv") + token.shouldBeEqualTo("tok_ubfj2q76miwundwlk72vxt2i7q") + expiresOn.shouldBeEqualTo("2019-08-24T14:15:22Z") + } + + assertFalse(response is NetworkApiResponse.ServerError) + + assertFalse(response is NetworkApiResponse.NetworkError) + + assertFalse(response is NetworkApiResponse.InternalError) + } + } + + @DisplayName("Get CVVToken Details API failed") + @ParameterizedTest(name = "{index}: The token API responds with an error for errorCode:{0}") + @ValueSource(ints = [401, 422, 502, 404]) + fun apiRespondsWithError(errorCode: Int) = runTest { + // Given + enqueueMockResponse( + code = errorCode, + json = GetTokenDetailsResponseTestJson.cvvTokenDetailsErrorResponse + ) + + // When + val response = tokenNetworkApiClient.sendCVVTokenRequest( + CVVToTokenNetworkRequestMapper().map(TokenizationRequestTestData.cvvTokenizationRequest) + ) + + // Then + launch { + // Then + assertFalse(response is NetworkApiResponse.Success) + + assertTrue(response is NetworkApiResponse.ServerError) + with((response as NetworkApiResponse.ServerError).body) { + this?.requestId.shouldNotBeNull() + this?.errorCodes.shouldNotBeNull() + this?.errorType.shouldNotBeNull() + } + + assertFalse(response is NetworkApiResponse.NetworkError) + + assertFalse(response is NetworkApiResponse.InternalError) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + @Test + @DisplayName("Get CVVToken Details API does not respond and throw network error") + fun apiDoesNotRespond() = runTest { + // Given + enqueueNoResponse() + + // When + val response = tokenNetworkApiClient.sendCVVTokenRequest( + CVVToTokenNetworkRequestMapper().map(TokenizationRequestTestData.cvvTokenizationRequest) ) launch { diff --git a/checkout/src/test/java/com/checkout/tokenization/logging/TokenizationEventLoggerTest.kt b/checkout/src/test/java/com/checkout/tokenization/logging/TokenizationEventLoggerTest.kt index d35a5b9d7..cbe761213 100644 --- a/checkout/src/test/java/com/checkout/tokenization/logging/TokenizationEventLoggerTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/logging/TokenizationEventLoggerTest.kt @@ -3,7 +3,7 @@ package com.checkout.tokenization.logging import com.checkout.eventlogger.domain.model.MonitoringLevel import com.checkout.logging.Logger import com.checkout.logging.model.LoggingEvent -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.error.TokenizationError import com.checkout.tokenization.utils.TokenizationConstants import io.mockk.impl.annotations.RelaxedMockK @@ -142,7 +142,11 @@ internal class TokenizationEventLoggerTest { val capturedEvent = slot() // When - tokenizationEventLogger.logTokenResponseEvent(tokenType, "test_key", CardTokenTestData.tokenDetailsResponse()) + tokenizationEventLogger.logTokenResponseEvent( + tokenType = tokenType, + publicKey = "test_key", + tokenDetails = TokenizationRequestTestData.tokenDetailsResponse() + ) // Then verify { mockLogger.log(capture(capturedEvent)) } @@ -178,7 +182,7 @@ internal class TokenizationEventLoggerTest { "test_key", null, 501, - CardTokenTestData.errorResponse() + TokenizationRequestTestData.errorResponse() ) // Then diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/CardTokenizationNetworkDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/CardTokenizationNetworkDataMapperTest.kt index 8ba79f85e..188b19528 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/CardTokenizationNetworkDataMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/CardTokenizationNetworkDataMapperTest.kt @@ -1,6 +1,6 @@ package com.checkout.tokenization.mapper -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.network.response.NetworkApiResponse import com.checkout.tokenization.mapper.request.AddressToAddressEntityDataMapper import com.checkout.tokenization.mapper.request.PhoneToPhoneEntityDataMapper @@ -62,7 +62,7 @@ internal class CardTokenizationNetworkDataMapperTest { @Test fun `unexpected response type is a failure`() { // Given - val unexpectedResponseObject = CardTokenTestData.card + val unexpectedResponseObject = TokenizationRequestTestData.card // When val mappedResult = @@ -92,9 +92,9 @@ internal class CardTokenizationNetworkDataMapperTest { issuerCountry = "US", productId = "F", productType = "CLASSIC", - billingAddress = AddressEntityToAddressDataMapper().map(CardTokenTestData.addressEntity), + billingAddress = AddressEntityToAddressDataMapper().map(TokenizationRequestTestData.addressEntity), phone = PhoneEntityToPhoneDataMapper().map( - from = CardTokenTestData.phoneEntity to CardTokenTestData.addressEntity.country + from = TokenizationRequestTestData.phoneEntity to TokenizationRequestTestData.addressEntity.country ), name = "Bruce Wayne" ) @@ -145,7 +145,7 @@ internal class CardTokenizationNetworkDataMapperTest { fun `map ServerError response to TokenResult`() { // Given every { mockServerErrorResponse.code } returns 400 - every { mockServerErrorResponse.body } returns CardTokenTestData.errorResponse() + every { mockServerErrorResponse.body } returns TokenizationRequestTestData.errorResponse() // When val mappedResult = cardTokenizationNetworkDataMapper.toTokenResult(mockServerErrorResponse) diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/request/AddressToAddressEntityDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/request/AddressToAddressEntityDataMapperTest.kt index 3f4fb2a5a..4dbb58be7 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/request/AddressToAddressEntityDataMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/request/AddressToAddressEntityDataMapperTest.kt @@ -1,6 +1,6 @@ package com.checkout.tokenization.mapper.request -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.entity.AddressEntity import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach @@ -27,7 +27,7 @@ internal class AddressToAddressEntityDataMapperTest { ) // When - val actualAddressEntityData = addressToAddressEntityDataMapper.map(CardTokenTestData.address) + val actualAddressEntityData = addressToAddressEntityDataMapper.map(TokenizationRequestTestData.address) // Then assertEquals(expectedAddressEntityData, actualAddressEntityData) diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapperTest.kt new file mode 100644 index 000000000..e78a32c56 --- /dev/null +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/request/CVVToTokenNetworkRequestMapperTest.kt @@ -0,0 +1,34 @@ +package com.checkout.tokenization.mapper.request + +import com.checkout.mock.TokenizationRequestTestData +import com.checkout.tokenization.entity.TokenDataEntity +import com.checkout.tokenization.request.CVVTokenNetworkRequest +import com.checkout.tokenization.utils.TokenizationConstants +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class CVVToTokenNetworkRequestMapperTest { + private lateinit var cvvToTokenNetworkRequestMapper: CVVToTokenNetworkRequestMapper + + @BeforeEach + fun setUp() { + cvvToTokenNetworkRequestMapper = CVVToTokenNetworkRequestMapper() + } + + @Test + fun `mapping of CVVTokenizationRequest to CVVTokenNetworkRequest data`() { + // Given + val expectedCVVTokenNetworkRequest = CVVTokenNetworkRequest( + TokenizationConstants.CVV, TokenDataEntity("123") + ) + + val request = TokenizationRequestTestData.cvvTokenizationRequest + + // When + val actualTokenRequestData = cvvToTokenNetworkRequestMapper.map(request) + + // Then + assertEquals(expectedCVVTokenNetworkRequest, actualTokenRequestData) + } +} diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/request/CardToTokenRequestMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/request/CardToTokenRequestMapperTest.kt index feeddf7fb..ac71926f2 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/request/CardToTokenRequestMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/request/CardToTokenRequestMapperTest.kt @@ -1,6 +1,6 @@ package com.checkout.tokenization.mapper.request -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.request.TokenRequest import com.checkout.tokenization.utils.TokenizationConstants import org.amshove.kluent.internal.assertEquals @@ -25,12 +25,12 @@ internal class CardToTokenRequestMapperTest { "25", "Bob martin", "123", - AddressToAddressEntityDataMapper().map(CardTokenTestData.address), - PhoneToPhoneEntityDataMapper().map(CardTokenTestData.phone) + AddressToAddressEntityDataMapper().map(TokenizationRequestTestData.address), + PhoneToPhoneEntityDataMapper().map(TokenizationRequestTestData.phone) ) // When - val actualTokenRequestData = cardTokenRequestMapper.map(CardTokenTestData.card) + val actualTokenRequestData = cardTokenRequestMapper.map(TokenizationRequestTestData.card) // Then assertEquals(expectedTokenRequestData, actualTokenRequestData) diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/request/PhoneToPhoneEntityDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/request/PhoneToPhoneEntityDataMapperTest.kt index 263be51fb..998336198 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/request/PhoneToPhoneEntityDataMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/request/PhoneToPhoneEntityDataMapperTest.kt @@ -1,6 +1,6 @@ package com.checkout.tokenization.mapper.request -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.entity.PhoneEntity import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach @@ -20,7 +20,7 @@ internal class PhoneToPhoneEntityDataMapperTest { val expectedPhoneEntityData = PhoneEntity("4155552671", "44") // When - val actualPhoneEntityData = phoneToPhoneEntityDataMapper.map(CardTokenTestData.phone) + val actualPhoneEntityData = phoneToPhoneEntityDataMapper.map(TokenizationRequestTestData.phone) // Then assertEquals(expectedPhoneEntityData, actualPhoneEntityData) diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/response/AddressEntityToAddressDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/response/AddressEntityToAddressDataMapperTest.kt index 08733d451..c701859a8 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/response/AddressEntityToAddressDataMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/response/AddressEntityToAddressDataMapperTest.kt @@ -1,7 +1,7 @@ package com.checkout.tokenization.mapper.response import com.checkout.base.model.Country -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.model.Address import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach @@ -29,7 +29,7 @@ internal class AddressEntityToAddressDataMapperTest { ) // When - val actualAddressData = addressEntityToAddressDataMapper.map(CardTokenTestData.addressEntity) + val actualAddressData = addressEntityToAddressDataMapper.map(TokenizationRequestTestData.addressEntity) // Then assertEquals(expectedAddressData, actualAddressData) diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapperTest.kt new file mode 100644 index 000000000..a00a7bca3 --- /dev/null +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/response/CVVTokenizationNetworkDataMapperTest.kt @@ -0,0 +1,124 @@ +package com.checkout.tokenization.mapper.response + +import com.checkout.mock.TokenizationRequestTestData +import com.checkout.network.response.NetworkApiResponse +import com.checkout.tokenization.model.CVVTokenDetails +import com.checkout.tokenization.model.CVVTokenizationRequest +import com.checkout.tokenization.model.TokenResult +import com.checkout.tokenization.response.GetCVVTokenDetailsResponse +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should be instance of` +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class CVVTokenizationNetworkDataMapperTest { + private lateinit var cvvTokenizationNetworkDataMapper: CVVTokenizationNetworkDataMapper + + @BeforeEach + fun setUp() { + cvvTokenizationNetworkDataMapper = CVVTokenizationNetworkDataMapper() + } + + @DisplayName("Mapping successful GetCVVTokenDetailsResponse to TokenResult") + @Nested + inner class MapToSuccessfulTokenResult { + + @MockK + private lateinit var mockGetCVVTokenDetailsResponse: GetCVVTokenDetailsResponse + + @Test + fun `map GetCVVTokenDetailsResponse to TokenResult`() { + // Given + val expectedResult = expectedCVVTokenDetails() + setupMockResponses(expectedResult) + + // When + val mappedResult = + cvvTokenizationNetworkDataMapper.toTokenResult( + NetworkApiResponse.Success(mockGetCVVTokenDetailsResponse) + ) + + // Then + mappedResult `should be instance of` TokenResult.Success::class + + val actualResult = (mappedResult as TokenResult.Success).result + + actualResult `should be equal to` expectedResult + } + + @Test + fun `unexpected response type is a failure`() { + // Given + val unexpectedResponseObject = TokenizationRequestTestData.cvvTokenizationRequest + + // When + val mappedResult = + cvvTokenizationNetworkDataMapper.toTokenResult(NetworkApiResponse.Success(unexpectedResponseObject)) + + // Then + mappedResult `should be instance of` TokenResult.Failure::class + + (mappedResult as TokenResult.Failure).error.message `should be equal to` + "${CVVTokenizationRequest::class.java.name} cannot be mapped to a CVVTokenDetails" + } + + private fun expectedCVVTokenDetails(): CVVTokenDetails = CVVTokenDetails( + type = "CVV", token = "tok_test", expiresOn = "2019-08-24T14:15:22Z" + ) + + private fun setupMockResponses( + tokenDetails: CVVTokenDetails, + ) { + every { mockGetCVVTokenDetailsResponse.type } returns tokenDetails.type + every { mockGetCVVTokenDetailsResponse.token } returns tokenDetails.token + every { mockGetCVVTokenDetailsResponse.expiresOn } returns tokenDetails.expiresOn + } + } + + @DisplayName("Mapping unsuccessful NetworkApiResponse") + @Nested + inner class MapToFailedTokenResult { + + @MockK + private lateinit var mockServerErrorResponse: NetworkApiResponse.ServerError + + @MockK + private lateinit var mockNetworkErrorResponse: NetworkApiResponse.NetworkError + + @Test + fun `map ServerError response to TokenResult`() { + // Given + every { mockServerErrorResponse.code } returns 400 + every { mockServerErrorResponse.body } returns TokenizationRequestTestData.errorResponse() + + // When + val mappedResult = cvvTokenizationNetworkDataMapper.toTokenResult(mockServerErrorResponse) + + // Then + mappedResult `should be instance of` TokenResult.Failure::class + (mappedResult as TokenResult.Failure).error.message `should be equal to` + "Token request failed - testErrorType (HttpStatus: 400)" + } + + @Test + fun `map NetworkError response to TokenResult`() { + // Given + val cause = Throwable("test exception") + every { mockNetworkErrorResponse.throwable } returns cause + + // When + val mappedResult = cvvTokenizationNetworkDataMapper.toTokenResult(mockNetworkErrorResponse) + + // Then + mappedResult `should be instance of` TokenResult.Failure::class + (mappedResult as TokenResult.Failure).error.cause `should be equal to` cause + } + } +} diff --git a/checkout/src/test/java/com/checkout/tokenization/mapper/response/PhoneEntityToPhoneDataMapperTest.kt b/checkout/src/test/java/com/checkout/tokenization/mapper/response/PhoneEntityToPhoneDataMapperTest.kt index 36561a3c1..0478865dd 100644 --- a/checkout/src/test/java/com/checkout/tokenization/mapper/response/PhoneEntityToPhoneDataMapperTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/mapper/response/PhoneEntityToPhoneDataMapperTest.kt @@ -1,7 +1,7 @@ package com.checkout.tokenization.mapper.response import com.checkout.base.model.Country -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.model.Phone import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach @@ -27,7 +27,7 @@ internal class PhoneEntityToPhoneDataMapperTest { ) // When - val actualPhoneData = phoneEntityToPhoneDataMapper.map(from = CardTokenTestData.phoneEntity to "GB") + val actualPhoneData = phoneEntityToPhoneDataMapper.map(from = TokenizationRequestTestData.phoneEntity to "GB") // Then assertEquals(expectedPhoneData.number, actualPhoneData.number) diff --git a/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt b/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt index 474ee5578..54d27ab63 100644 --- a/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt @@ -1,17 +1,24 @@ package com.checkout.tokenization.repository +import com.checkout.base.model.CardScheme import com.checkout.base.usecase.UseCase -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.network.response.ErrorResponse import com.checkout.network.response.NetworkApiResponse import com.checkout.tokenization.TokenNetworkApiClient import com.checkout.tokenization.error.TokenizationError import com.checkout.tokenization.logging.TokenizationLogger +import com.checkout.tokenization.mapper.request.CVVToTokenNetworkRequestMapper import com.checkout.tokenization.mapper.request.CardToTokenRequestMapper +import com.checkout.tokenization.mapper.response.CVVTokenizationNetworkDataMapper import com.checkout.tokenization.mapper.response.CardTokenizationNetworkDataMapper +import com.checkout.tokenization.model.CVVTokenizationRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler import com.checkout.tokenization.model.Card import com.checkout.tokenization.model.CardTokenRequest import com.checkout.tokenization.model.GooglePayTokenRequest +import com.checkout.tokenization.model.ValidateCVVTokenizationRequest +import com.checkout.tokenization.response.CVVTokenDetailsResponse import com.checkout.tokenization.response.TokenDetailsResponse import com.checkout.tokenization.utils.TokenizationConstants import com.checkout.validation.model.ValidationResult @@ -47,6 +54,10 @@ internal class TokenRepositoryImplTest { @RelaxedMockK private lateinit var mockValidateTokenizationDataUseCase: UseCase> + @RelaxedMockK + private lateinit var mockValidateCVVTokenizationDataUseCase: + UseCase> + @RelaxedMockK private lateinit var mockTokenizationLogger: TokenizationLogger @@ -55,12 +66,15 @@ internal class TokenRepositoryImplTest { @BeforeEach fun setUp() { tokenRepositoryImpl = TokenRepositoryImpl( - mockTokenNetworkApiClient, - CardToTokenRequestMapper(), - CardTokenizationNetworkDataMapper(), - mockValidateTokenizationDataUseCase, - mockTokenizationLogger, - "test_key" + networkApiClient = mockTokenNetworkApiClient, + cardToTokenRequestMapper = CardToTokenRequestMapper(), + cvvToTokenNetworkRequestMapper = CVVToTokenNetworkRequestMapper(), + cardTokenizationNetworkDataMapper = CardTokenizationNetworkDataMapper(), + validateTokenizationDataUseCase = mockValidateTokenizationDataUseCase, + validateCVVTokenizationDataUseCase = mockValidateCVVTokenizationDataUseCase, + logger = mockTokenizationLogger, + publicKey = "test_key", + cvvTokenizationNetworkDataMapper = CVVTokenizationNetworkDataMapper() ) } @@ -71,7 +85,7 @@ internal class TokenRepositoryImplTest { fun `when sendCardTokenRequest invoked with success response then success handler invoked`() { testCardTokenResultInvocation( true, - NetworkApiResponse.Success(CardTokenTestData.tokenDetailsResponse()) + NetworkApiResponse.Success(TokenizationRequestTestData.tokenDetailsResponse()) ) } @@ -131,7 +145,7 @@ internal class TokenRepositoryImplTest { // When tokenRepositoryImpl.sendCardTokenRequest( CardTokenRequest( - CardTokenTestData.card, + TokenizationRequestTestData.card, onSuccess = { }, onFailure = { } ) @@ -169,7 +183,7 @@ internal class TokenRepositoryImplTest { // When tokenRepositoryImpl.sendCardTokenRequest( CardTokenRequest( - CardTokenTestData.card, + TokenizationRequestTestData.card, onSuccess = { isSuccess = true }, onFailure = { isSuccess = false } ) @@ -205,7 +219,7 @@ internal class TokenRepositoryImplTest { // When tokenRepositoryImpl.sendCardTokenRequest( CardTokenRequest( - CardTokenTestData.card, + TokenizationRequestTestData.card, onSuccess = { }, onFailure = { } ) @@ -244,7 +258,7 @@ internal class TokenRepositoryImplTest { fun `when sendGooglePayTokenRequest invoked with success response then success handler invoked`() { testGooglePayTokenResultInvocation( true, - NetworkApiResponse.Success(CardTokenTestData.tokenDetailsResponse()) + NetworkApiResponse.Success(TokenizationRequestTestData.tokenDetailsResponse()) ) } @@ -502,4 +516,89 @@ internal class TokenRepositoryImplTest { } } } + + @DisplayName("CVVToken Details invocation") + @Nested + inner class GetCVVTokenNetworkRequestDetails { + @Test + fun `when sendCVVTokenizationRequest invoked with success response then success handler invoked`() { + testCVVTokenResultInvocation( + successHandlerInvoked = true, + response = NetworkApiResponse.Success( + body = CVVTokenDetailsResponse( + type = "cvv", + token = "test_token", + expiresOn = "2019-08-24T14:15:22Z" + ) + ) + ) + } + + @Test + fun `when sendCVVTokenizationRequest invoked with network error response then failure handler invoked`() { + testCVVTokenResultInvocation( + successHandlerInvoked = false, + response = NetworkApiResponse.NetworkError(Exception("Network connection lost")) + ) + } + + @Test + fun `when sendCVVTokenizationRequest invoked with server error response then failure handler invoked`() { + testCVVTokenResultInvocation( + successHandlerInvoked = false, + response = NetworkApiResponse.ServerError(body = null, code = 123) + ) + } + + @Test + fun `when sendCVVTokenizationRequest invoked with internal error response then failure handler invoked`() { + testCVVTokenResultInvocation( + successHandlerInvoked = false, + response = NetworkApiResponse.InternalError( + TokenizationError( + errorCode = "internal_error", + message = "exception.message", + cause = null + ) + ) + ) + } + + private fun testCVVTokenResultInvocation( + successHandlerInvoked: Boolean, + response: NetworkApiResponse + ) = + runTest { + // Given + var isSuccess: Boolean? = null + + val testDispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(testDispatcher) + + tokenRepositoryImpl.networkCoroutineScope = CoroutineScope(StandardTestDispatcher(testScheduler)) + + coEvery { mockValidateCVVTokenizationDataUseCase.execute(any()) } returns ValidationResult.Success(Unit) + + coEvery { mockTokenNetworkApiClient.sendCVVTokenRequest(any()) } returns response + + // When + tokenRepositoryImpl.sendCVVTokenizationRequest( + CVVTokenizationRequest( + cvv = "123", + cardScheme = CardScheme.VISA, + resultHandler = { result -> + isSuccess = when (result) { + is CVVTokenizationResultHandler.Success -> true + is CVVTokenizationResultHandler.Failure -> false + } + } + ) + ) + + // Then + launch { + assertEquals(isSuccess.toString(), successHandlerInvoked.toString()) + } + } + } } diff --git a/checkout/src/test/java/com/checkout/validation/validator/CVVComponentDetailsValidatorTest.kt b/checkout/src/test/java/com/checkout/validation/validator/CVVComponentDetailsValidatorTest.kt index c5b22ad50..87012babb 100644 --- a/checkout/src/test/java/com/checkout/validation/validator/CVVComponentDetailsValidatorTest.kt +++ b/checkout/src/test/java/com/checkout/validation/validator/CVVComponentDetailsValidatorTest.kt @@ -1,12 +1,17 @@ package com.checkout.validation.validator +import com.checkout.base.error.CheckoutError import com.checkout.base.model.CardScheme import com.checkout.validation.api.CVVComponentValidator +import com.checkout.validation.error.ValidationError.Companion.CVV_INVALID_LENGTH import com.checkout.validation.model.CvvValidationRequest +import com.checkout.validation.model.ValidationResult import com.checkout.validation.validator.contract.Validator +import io.mockk.every import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.verify +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -25,7 +30,7 @@ internal class CVVComponentDetailsValidatorTest { } @Test - fun `when validateCvv is invoked then validation with correct CvvValidationRequest requested`() { + fun `when validate is invoked then validation with correct CvvValidationRequest requested`() { // Given val mockCvv = "123" val mockCardScheme = CardScheme.JCB @@ -37,4 +42,23 @@ internal class CVVComponentDetailsValidatorTest { // Then verify { mockCvvValidator.validate(eq(expected)) } } + + @Test + fun `when validate is invoked with invalid cvv then validation with failure result is returned`() { + // Given + val mockCvv = "" + val mockCardScheme = CardScheme.JCB + val expectedResult = ValidationResult.Failure(CheckoutError(CVV_INVALID_LENGTH)) + every { + mockCvvValidator.validate( + CvvValidationRequest(mockCvv, mockCardScheme) + ) + } returns ValidationResult.Failure(CheckoutError(CVV_INVALID_LENGTH)) + + // When + val result = cvvComponentValidator.validate(mockCvv, mockCardScheme) + + // Then + assertEquals(expectedResult.error.errorCode, (result as? ValidationResult.Failure)?.error?.errorCode) + } } diff --git a/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateCVVTokenizationDataUseCaseTest.kt b/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateCVVTokenizationDataUseCaseTest.kt new file mode 100644 index 000000000..16fc4bcd6 --- /dev/null +++ b/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateCVVTokenizationDataUseCaseTest.kt @@ -0,0 +1,96 @@ +package com.checkout.validation.validator.usecase + +import com.checkout.base.error.CheckoutError +import com.checkout.base.model.CardScheme +import com.checkout.base.usecase.UseCase +import com.checkout.tokenization.model.ValidateCVVTokenizationRequest +import com.checkout.tokenization.usecase.ValidateCVVTokenizationDataUseCase +import com.checkout.validation.api.CVVComponentValidator +import com.checkout.validation.error.ValidationError +import com.checkout.validation.model.ValidationResult +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class ValidateCVVTokenizationDataUseCaseTest { + + @RelaxedMockK + lateinit var mockCVVComponentValidator: CVVComponentValidator + + private lateinit var validateCVVTokenizationDataUseCase: + UseCase> + + @BeforeEach + fun setUp() { + validateCVVTokenizationDataUseCase = + ValidateCVVTokenizationDataUseCase(cvvComponentValidator = mockCVVComponentValidator) + every { mockCVVComponentValidator.validate(any(), any()) } returns ValidationResult.Success(Unit) + } + + @Test + fun `when invalid cvv is requested then return failure result`() { + // Given + val mockRequest = mockk() + val cvv = "???" + val cardScheme = CardScheme.VISA + val expectedResult = ValidationResult.Failure( + CheckoutError( + errorCode = ValidationError.CVV_INVALID_LENGTH, message = "Please enter a valid security code" + ) + ) + + every { mockRequest.cvv } returns cvv + every { mockRequest.cardScheme } returns cardScheme + + every { + mockCVVComponentValidator.validate( + eq(cvv), eq(cardScheme) + ) + } returns ValidationResult.Failure( + CheckoutError( + errorCode = ValidationError.CVV_INVALID_LENGTH, message = "Please enter a valid security code" + ) + ) + + // When + val result = validateCVVTokenizationDataUseCase.execute(mockRequest) + + // Then + assertTrue(result is ValidationResult.Failure) + val errorValidationResult = (result as? ValidationResult.Failure) + assertEquals(expectedResult.error.errorCode, errorValidationResult?.error?.errorCode) + assertEquals(expectedResult.error.message, errorValidationResult?.error?.message) + } + + @Test + fun `when valid cvv is requested then return success result`() { + // Given + val mockRequest = mockk() + val cvv = "123" + val cardScheme = CardScheme.VISA + val expectedResult = ValidationResult.Success(Unit) + + every { mockRequest.cvv } returns cvv + every { mockRequest.cardScheme } returns cardScheme + + every { + mockCVVComponentValidator.validate( + eq(cvv), eq(cardScheme) + ) + } returns ValidationResult.Success(Unit) + + // When + val result = validateCVVTokenizationDataUseCase.execute(mockRequest) + + // Then + assertTrue(result is ValidationResult.Success) + assertEquals(expectedResult.value, (result as? ValidationResult.Success)?.value) + } +} diff --git a/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateTokenizationDataUseCaseTest.kt b/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateTokenizationDataUseCaseTest.kt index d96e99de0..84a3dc056 100644 --- a/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateTokenizationDataUseCaseTest.kt +++ b/checkout/src/test/java/com/checkout/validation/validator/usecase/ValidateTokenizationDataUseCaseTest.kt @@ -3,7 +3,7 @@ package com.checkout.validation.validator.usecase import com.checkout.base.error.CheckoutError import com.checkout.base.model.CardScheme import com.checkout.base.usecase.UseCase -import com.checkout.mock.CardTokenTestData +import com.checkout.mock.TokenizationRequestTestData import com.checkout.tokenization.mapper.request.AddressToAddressValidationRequestDataMapper import com.checkout.tokenization.model.Address import com.checkout.tokenization.model.Card @@ -72,16 +72,17 @@ internal class ValidateTokenizationDataUseCaseTest { every { addressToAddressValidationRequestDataMapper.map(any()) - } returns CardTokenTestData.addressValidationRequest + } returns TokenizationRequestTestData.addressValidationRequest - every { mockAddressValidator.validate(any()) } returns ValidationResult.Success(CardTokenTestData.address) - every { mockPhoneValidator.validate(any()) } returns ValidationResult.Success(CardTokenTestData.phone) + every { mockAddressValidator.validate(any()) } returns + ValidationResult.Success(TokenizationRequestTestData.address) + every { mockPhoneValidator.validate(any()) } returns ValidationResult.Success(TokenizationRequestTestData.phone) } @Test fun `when valid card data is requested then return success result`() { // Given - val mockRequest = CardTokenTestData.card + val mockRequest = TokenizationRequestTestData.card val captureAddress = slot() val capturePhone = slot() @@ -116,7 +117,7 @@ internal class ValidateTokenizationDataUseCaseTest { @Test fun `when valid card data with empty cvv number is requested then return success result`() { // Given - val mockRequest = CardTokenTestData.card.copy(cvv = "") + val mockRequest = TokenizationRequestTestData.card.copy(cvv = "") every { mockCardValidator.validateCvv(any(), any()) } returns ValidationResult.Failure(CheckoutError("1")) // When @@ -206,7 +207,7 @@ internal class ValidateTokenizationDataUseCaseTest { // Given val mockRequest = mockk() val expectedResult = ValidationResult.Failure(CheckoutError("123")) - val address = CardTokenTestData.invalidAddress + val address = TokenizationRequestTestData.invalidAddress every { mockRequest.number } returns "mockNumber" every { mockRequest.cvv } returns "cvv" @@ -230,12 +231,12 @@ internal class ValidateTokenizationDataUseCaseTest { // Given val mockRequest = mockk() val expectedResult = ValidationResult.Failure(CheckoutError("123")) - val phone = CardTokenTestData.inValidPhone + val phone = TokenizationRequestTestData.inValidPhone every { mockRequest.number } returns "mockNumber" every { mockRequest.cvv } returns "cvv" every { mockRequest.expiryDate } returns ExpiryDate(10, 25) - every { mockRequest.billingAddress } returns CardTokenTestData.address + every { mockRequest.billingAddress } returns TokenizationRequestTestData.address every { mockRequest.phone } returns phone every { diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/styling/CustomCVVInputFieldStyle.kt b/example_app_frames/src/main/java/com/checkout/example/frames/styling/CustomCVVInputFieldStyle.kt index 4fa76cc59..63be1c705 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/styling/CustomCVVInputFieldStyle.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/styling/CustomCVVInputFieldStyle.kt @@ -5,7 +5,7 @@ import com.checkout.example.frames.paymentformstyling.PaymentFormConstants.place import com.checkout.example.frames.paymentformstyling.PaymentFormConstants.textColor import com.checkout.frames.R import com.checkout.frames.model.CornerRadius -import com.checkout.frames.model.Margin +import com.checkout.frames.model.Padding import com.checkout.frames.model.Shape import com.checkout.frames.model.font.Font import com.checkout.frames.style.component.base.ContainerStyle @@ -22,9 +22,9 @@ object CustomCVVInputFieldStyle { textStyle = TextStyle(16, color = textColor, font = Font.Cursive), placeholderStyle = TextStyle(16, color = placeHolderTextColor, font = Font.Cursive), containerStyle = ContainerStyle( - width = 290, - margin = Margin(end = 10), + width = 250, color = backgroundColor, + padding = Padding(end = 2), shape = Shape.RoundCorner, cornerRadius = CornerRadius(9) ), diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/CustomButton.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/CustomButton.kt index de2e91544..721fcdc83 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/CustomButton.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/CustomButton.kt @@ -7,15 +7,20 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color +@Suppress("MagicNumber") @Composable -fun CustomButton(isCVVValid: MutableState) { +fun CustomButton( + isCVVValid: MutableState, + buttonClick: () -> Unit, +) { Button( onClick = { - // TODO: Tokenization - }, enabled = isCVVValid.value, + buttonClick.invoke() + }, + enabled = isCVVValid.value, colors = ButtonDefaults.buttonColors( - contentColor = Color.White, containerColor = Color.Green, disabledContentColor = Color.Gray - ) + contentColor = Color.White, containerColor = Color(0xFF0B5FF0), disabledContentColor = Color.Gray + ), ) { Text(text = "Pay") } diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/LoadCVVComponent.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/LoadCVVComponent.kt new file mode 100644 index 000000000..a9a51cec1 --- /dev/null +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/LoadCVVComponent.kt @@ -0,0 +1,49 @@ +package com.checkout.example.frames.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.checkout.example.frames.ui.extension.showAlertDialog +import com.checkout.frames.R +import com.checkout.frames.cvvinputfield.api.CVVComponentMediator +import com.checkout.tokenization.model.CVVTokenizationResultHandler + +@Composable +fun LoadCVVComponent( + cvvComponentMediator: CVVComponentMediator, + isEnteredCVVValid: MutableState = mutableStateOf(true), +) { + val context = LocalContext.current + Row( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalArrangement = Arrangement.SpaceAround, + ) { + cvvComponentMediator.CVVComponent() + + CustomButton(isCVVValid = isEnteredCVVValid) { + cvvComponentMediator.createToken { result -> + when (result) { + is CVVTokenizationResultHandler.Success -> { + val tokenDetails = result.tokenDetails + context.showAlertDialog( + title = context.getString(R.string.token_generated), message = tokenDetails.token + ) + } + + is CVVTokenizationResultHandler.Failure -> { + val errorMessage = result.errorMessage + context.showAlertDialog( + title = context.getString(R.string.token_generated_failed), message = errorMessage + ) + } + } + } + } + } +} diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/ThreedsComponent.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/ThreedsComponent.kt index ca1c57331..aaf201d62 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/ThreedsComponent.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/component/ThreedsComponent.kt @@ -29,13 +29,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.checkout.example.frames.ui.extension.showAlertDialog import com.checkout.example.frames.ui.screen.ThreedSecureActivity import com.checkout.example.frames.ui.theme.ButtonBorder import com.checkout.example.frames.ui.theme.DarkBlue import com.checkout.example.frames.ui.theme.GrayColor import com.checkout.example.frames.ui.utils.CORNER_RADIUS_PERCENT -import com.checkout.example.frames.ui.utils.PromptUtils -import com.checkout.example.frames.ui.utils.PromptUtils.neutralButton import com.checkout.example.frames.ui.utils.URL_IDENTIFIER import com.checkout.frames.R @@ -78,8 +77,8 @@ fun ThreedComponent(context: Context) { intent.putExtra(URL_IDENTIFIER, textValue.text) context.startActivity(intent) } else { - showAlertDialog( - context, context.getString(R.string.empty_url), context.getString(R.string.paste_valid_link) + context.showAlertDialog( + context.getString(R.string.empty_url), context.getString(R.string.paste_valid_link) ) } }, @@ -97,13 +96,3 @@ fun ThreedComponent(context: Context) { } } } - -fun showAlertDialog(context: Context, title: String, message: String) { - PromptUtils.alertDialog(context) { - setTitle(title) - setMessage(message) - neutralButton { - it.dismiss() - } - }.show() -} diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/extension/showAlertDialog.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/extension/showAlertDialog.kt new file mode 100644 index 000000000..1dd17fd31 --- /dev/null +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/extension/showAlertDialog.kt @@ -0,0 +1,15 @@ +package com.checkout.example.frames.ui.extension + +import android.content.Context +import com.checkout.example.frames.ui.utils.PromptUtils +import com.checkout.example.frames.ui.utils.PromptUtils.neutralButton + +fun Context.showAlertDialog(title: String, message: String) { + PromptUtils.alertDialog(this) { + setTitle(title) + setMessage(message) + neutralButton { + it.dismiss() + } + }.show() +} diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/CVVTokenizationScreen.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/CVVTokenizationScreen.kt index 9c5d440b3..faf26bdaf 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/CVVTokenizationScreen.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/CVVTokenizationScreen.kt @@ -2,34 +2,45 @@ package com.checkout.example.frames.ui.screen import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.checkout.base.model.CardScheme import com.checkout.base.model.Environment +import com.checkout.example.frames.paymentformstyling.PaymentFormConstants import com.checkout.example.frames.styling.CustomCVVInputFieldStyle -import com.checkout.example.frames.ui.utils.PUBLIC_KEY +import com.checkout.example.frames.ui.utils.PUBLIC_KEY_CVV_TOKENIZATION import com.checkout.example.frames.ui.viewmodel.CVVTokenizationViewModel import com.checkout.frames.cvvinputfield.CVVComponentApiFactory import com.checkout.frames.cvvinputfield.api.CVVComponentApi import com.checkout.frames.cvvinputfield.api.CVVComponentMediator import com.checkout.frames.cvvinputfield.models.CVVComponentConfig import com.checkout.frames.cvvinputfield.style.DefaultCVVInputFieldStyle -import com.checkout.frames.model.Margin +import com.checkout.frames.model.CornerRadius +import com.checkout.frames.model.Padding +import com.checkout.frames.model.Shape +import com.checkout.frames.model.font.Font import com.checkout.frames.style.component.base.ContainerStyle import com.checkout.frames.style.component.base.InputFieldStyle +import com.checkout.frames.style.component.base.TextStyle -@Suppress("MagicNumber", "LongMethod") +@Suppress("MagicNumber") @Composable fun CVVTokenizationScreen(navController: NavHostController) { val cvvTokenizationViewModel: CVVTokenizationViewModel = viewModel() - val cvvComponentApi = CVVComponentApiFactory.create(PUBLIC_KEY, Environment.SANDBOX, LocalContext.current) + + val cvvComponentApi = CVVComponentApiFactory.create( + publicKey = PUBLIC_KEY_CVV_TOKENIZATION, + environment = Environment.SANDBOX, + context = LocalContext.current + ) val visaMediator = createMediator( cvvComponentApi = cvvComponentApi, schemeValue = "Visa", inputFieldStyle = DefaultCVVInputFieldStyle.create().copy( - containerStyle = ContainerStyle(width = 290, margin = Margin(end = 10)) + containerStyle = ContainerStyle(width = 250, padding = Padding(end = 10)) ), enteredVisaCVVUpdated = cvvTokenizationViewModel.isEnteredVisaCVVValid ) @@ -41,14 +52,51 @@ fun CVVTokenizationScreen(navController: NavHostController) { enteredVisaCVVUpdated = cvvTokenizationViewModel.isEnteredMaestroCVVValid ) - LoadCVVComponentsContents(navController, cvvTokenizationViewModel, visaMediator, maestroMediator) + val amexMediator = createMediator( + cvvComponentApi = cvvComponentApi, + schemeValue = "AMERICAN_EXPRESS", + inputFieldStyle = CustomCVVInputFieldStyle.create().copy( + containerStyle = ContainerStyle( + width = 250, + color = 0XFF24302D, + shape = Shape.Rectangle, + padding = Padding(end = 2), + cornerRadius = CornerRadius(9) + ), + textStyle = TextStyle(16, color = 0XFF00CC2D, font = Font.Serif) + ), + enteredVisaCVVUpdated = cvvTokenizationViewModel.isEnteredAmexCVVValid + ) + + val unknownMediator = createMediator( + cvvComponentApi = cvvComponentApi, + schemeValue = "unknown", + inputFieldStyle = CustomCVVInputFieldStyle.create().copy( + containerStyle = ContainerStyle( + width = 250, + padding = Padding(end = 2), + color = PaymentFormConstants.backgroundColor, + shape = Shape.Circle, + cornerRadius = CornerRadius(9) + ) + ), + ) + + LoadCVVComponentsContents( + navController = navController, + cvvTokenizationViewModel = cvvTokenizationViewModel, + visaMediator = visaMediator, + maestroMediator = maestroMediator, + amexMediator = amexMediator, + unknownMediator = unknownMediator + ) } fun createMediator( cvvComponentApi: CVVComponentApi, schemeValue: String, inputFieldStyle: InputFieldStyle, - enteredVisaCVVUpdated: MutableState, + enteredVisaCVVUpdated: MutableState = mutableStateOf(true), ): CVVComponentMediator { val cvvComponentConfig = CVVComponentConfig( cardScheme = CardScheme.fromString(cardSchemeValue = schemeValue), onCVVValueChange = { isValidCVV -> diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/LoadCVVComponentsContents.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/LoadCVVComponentsContents.kt index b37f381da..19d2d7aa8 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/LoadCVVComponentsContents.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/screen/LoadCVVComponentsContents.kt @@ -15,14 +15,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.checkout.example.frames.ui.component.ClickableImage -import com.checkout.example.frames.ui.component.CustomButton +import com.checkout.example.frames.ui.component.LoadCVVComponent import com.checkout.example.frames.ui.component.TextComponent import com.checkout.example.frames.ui.theme.DarkBlue import com.checkout.example.frames.ui.theme.FramesTheme @@ -36,6 +35,8 @@ fun LoadCVVComponentsContents( cvvTokenizationViewModel: CVVTokenizationViewModel, visaMediator: CVVComponentMediator, maestroMediator: CVVComponentMediator, + amexMediator: CVVComponentMediator, + unknownMediator: CVVComponentMediator, ) { FramesTheme { Surface( @@ -47,7 +48,6 @@ fun LoadCVVComponentsContents( .verticalScroll(state = rememberScrollState()) .padding(start = 10.dp, end = 24.dp, bottom = 14.dp), verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start ) { Row( @@ -72,31 +72,35 @@ fun LoadCVVComponentsContents( Spacer(Modifier.height(15.dp)) - Text("Default CVV Component") + Text("Default CVV Component (Visa scheme along with pay button validation)") Spacer(Modifier.height(15.dp)) - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start - ) { - visaMediator.CVVComponent() + LoadCVVComponent(visaMediator, cvvTokenizationViewModel.isEnteredVisaCVVValid) - CustomButton(isCVVValid = cvvTokenizationViewModel.isEnteredVisaCVVValid) - } + Spacer(Modifier.height(30.dp)) + + Text("Custom CVV Component (Maestro scheme along with pay button validation)") Spacer(Modifier.height(15.dp)) - Text("Custom CVV Component") + LoadCVVComponent(maestroMediator, cvvTokenizationViewModel.isEnteredMaestroCVVValid) + + Spacer(Modifier.height(30.dp)) + + Text("Custom CVV Component (AMEX scheme along with pay button validation)") Spacer(Modifier.height(15.dp)) - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start - ) { - maestroMediator.CVVComponent() + LoadCVVComponent(amexMediator, cvvTokenizationViewModel.isEnteredAmexCVVValid) - CustomButton(isCVVValid = cvvTokenizationViewModel.isEnteredMaestroCVVValid) - } + Spacer(Modifier.height(30.dp)) + + Text("Custom CVV Component (unknown scheme without pay button validation)") + + Spacer(Modifier.height(15.dp)) + + LoadCVVComponent(unknownMediator) } } } diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/utils/Constants.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/utils/Constants.kt index f0eec8a1a..f71355537 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/utils/Constants.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/utils/Constants.kt @@ -12,6 +12,11 @@ val ENVIRONMENT: Environment = Environment.SANDBOX */ const val PUBLIC_KEY = "pk_test_b37b8b6b-fc9a-483f-a77e-3386b606f90e" +/** + * Replace with public key from Hub in Sandbox Environment, testing key for CVV Tokenization + */ +const val PUBLIC_KEY_CVV_TOKENIZATION = "pk_6b30805a-1f3b-4c63-8b75-eb3030109173" + /** * Replace with Secret key from Hub in Sandbox Environment */ diff --git a/example_app_frames/src/main/java/com/checkout/example/frames/ui/viewmodel/CVVTokenizationViewModel.kt b/example_app_frames/src/main/java/com/checkout/example/frames/ui/viewmodel/CVVTokenizationViewModel.kt index da7d830d0..dd4e91f8c 100644 --- a/example_app_frames/src/main/java/com/checkout/example/frames/ui/viewmodel/CVVTokenizationViewModel.kt +++ b/example_app_frames/src/main/java/com/checkout/example/frames/ui/viewmodel/CVVTokenizationViewModel.kt @@ -7,4 +7,5 @@ import androidx.lifecycle.ViewModel class CVVTokenizationViewModel : ViewModel() { val isEnteredVisaCVVValid: MutableState = mutableStateOf(false) val isEnteredMaestroCVVValid: MutableState = mutableStateOf(false) + val isEnteredAmexCVVValid: MutableState = mutableStateOf(false) } diff --git a/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/CVVInputFieldTest.kt b/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/CVVInputFieldTest.kt index 51a0ef170..7d1328023 100644 --- a/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/CVVInputFieldTest.kt +++ b/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/CVVInputFieldTest.kt @@ -17,14 +17,20 @@ internal class CVVInputFieldTest { val composeTestRule = createComposeRule() @Test - fun testCVVInputField() { + fun testCVVInputFieldTextChange() { // When + var cvvText = "" composeTestRule.setContent { - CVVInputField(config = CVVComponentConfig(CardScheme.VISA, {}, InputFieldStyle())) + CVVInputField( + config = CVVComponentConfig(CardScheme.VISA, {}, InputFieldStyle()), + ) { + cvvText = it + } } // Then composeTestRule.onNodeWithText("").assertIsDisplayed().assertIsNotFocused().performTextInput("123") composeTestRule.onNodeWithText("123").assertIsDisplayed().assertIsFocused() + assert(cvvText == "123") } } diff --git a/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt b/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorUITest.kt similarity index 80% rename from frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt rename to frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorUITest.kt index 0e00cb7a1..2c6b61a77 100644 --- a/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt +++ b/frames/src/androidTest/kotlin/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorUITest.kt @@ -3,17 +3,19 @@ package com.checkout.frames.cvvinputfield import android.content.Context import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.platform.app.InstrumentationRegistry +import com.checkout.CheckoutApiServiceFactory import com.checkout.base.model.CardScheme import com.checkout.base.model.Environment import com.checkout.frames.cvvinputfield.api.InternalCVVComponentMediator import com.checkout.frames.cvvinputfield.models.CVVComponentConfig +import com.checkout.frames.cvvinputfield.usecase.CVVTokenizationUseCase import com.checkout.frames.style.component.base.InputFieldStyle import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.jupiter.api.Assertions.assertTrue -internal class InternalCVVComponentMediatorTest { +internal class InternalCVVComponentMediatorUITest { @get:Rule val composeTestRule = createComposeRule() @@ -26,9 +28,12 @@ internal class InternalCVVComponentMediatorTest { context = InstrumentationRegistry.getInstrumentation().targetContext cvvComponentConfig = CVVComponentConfig(CardScheme.VISA, {}, InputFieldStyle()) cvvComponentMediator = InternalCVVComponentMediator( - cvvComponentConfig = cvvComponentConfig, publicKey = "", - environment = Environment.SANDBOX, - context = context, + cvvComponentConfig = cvvComponentConfig, + cvvTokenizationUseCase = CVVTokenizationUseCase( + CheckoutApiServiceFactory.create( + publicKey = "", environment = Environment.SANDBOX, context = context + ) + ), ) } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVInputField.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVInputField.kt index 9a96dd1c8..93c369396 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVInputField.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVInputField.kt @@ -18,9 +18,9 @@ import java.util.Random @Composable internal fun CVVInputField( config: CVVComponentConfig, + onValueChange: (String) -> Unit, ) { val uniqueKey = remember { "${System.currentTimeMillis()}_${Random().nextInt()}" } - val viewModel: CVVInputFieldViewModel = viewModel( key = uniqueKey, factory = CVVInputFieldViewModelFactory( @@ -35,5 +35,7 @@ internal fun CVVInputField( ) ) + onValueChange(viewModel.cvvInputFieldState.inputFieldState.text.value) + InputField(viewModel.cvvInputFieldStyle, viewModel.cvvInputFieldState.inputFieldState, viewModel::onCvvChange) } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/CVVComponentMediator.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/CVVComponentMediator.kt index 6be74d62e..b4c0e6801 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/CVVComponentMediator.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/CVVComponentMediator.kt @@ -1,9 +1,10 @@ package com.checkout.frames.cvvinputfield.api import android.view.View +import androidx.annotation.UiThread import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ViewCompositionStrategy -import com.checkout.tokenization.model.CVVTokenRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler /** * CVVComponent Mediator provides capabilities to load CVV component along with tokenization @@ -28,9 +29,10 @@ public interface CVVComponentMediator { ): View /** - * Creates token for CVV + * Creates a CVV token and invokes the provided [resultHandler] with the tokenization result. * - * @param request - [CVVTokenRequest] contains result handlers + * @param resultHandler - A lambda function that takes a [CVVTokenizationResultHandler] parameter. */ - public fun createToken(request: CVVTokenRequest) + @UiThread + public fun createToken(resultHandler: (CVVTokenizationResultHandler) -> Unit) } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentApi.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentApi.kt index 8956dee35..0b254053e 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentApi.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentApi.kt @@ -1,8 +1,10 @@ package com.checkout.frames.cvvinputfield.api import android.content.Context +import com.checkout.CheckoutApiServiceFactory import com.checkout.base.model.Environment import com.checkout.frames.cvvinputfield.models.CVVComponentConfig +import com.checkout.frames.cvvinputfield.usecase.CVVTokenizationUseCase internal class InternalCVVComponentApi( private val publicKey: String, @@ -11,6 +13,11 @@ internal class InternalCVVComponentApi( ) : CVVComponentApi { override fun createComponentMediator(cvvComponentConfig: CVVComponentConfig) = InternalCVVComponentMediator( - cvvComponentConfig = cvvComponentConfig, publicKey = publicKey, environment = environment, context = context + cvvComponentConfig = cvvComponentConfig, + cvvTokenizationUseCase = CVVTokenizationUseCase( + checkoutApiService = CheckoutApiServiceFactory.create( + publicKey = publicKey, environment = environment, context = context + ) + ) ) } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentMediator.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentMediator.kt index c8b56409b..814cd7935 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentMediator.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/api/InternalCVVComponentMediator.kt @@ -1,6 +1,5 @@ package com.checkout.frames.cvvinputfield.api -import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable @@ -9,20 +8,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import com.checkout.base.model.Environment +import com.checkout.base.usecase.UseCase import com.checkout.frames.cvvinputfield.CVVInputField import com.checkout.frames.cvvinputfield.models.CVVComponentConfig -import com.checkout.tokenization.model.CVVTokenRequest +import com.checkout.frames.cvvinputfield.models.InternalCVVTokenRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler -@Suppress("UnusedPrivateMember") internal class InternalCVVComponentMediator( private val cvvComponentConfig: CVVComponentConfig, - private val publicKey: String, - private val environment: Environment, - private val context: Context + private val cvvTokenizationUseCase: UseCase, ) : CVVComponentMediator { private val isCVVComponentCalled: MutableState = mutableStateOf(false) + private val cvvInputFieldTextValue: MutableState = mutableStateOf("") @Composable override fun CVVComponent() { @@ -33,13 +31,21 @@ internal class InternalCVVComponentMediator( private fun InternalCVVComponent() { val isCVVComponentAlreadyLoaded = remember { isCVVComponentCalled.value } if (!isCVVComponentAlreadyLoaded) { - CVVInputField(cvvComponentConfig) + CVVInputField(cvvComponentConfig) { onValueChange -> + cvvInputFieldTextValue.value = onValueChange + } isCVVComponentCalled.value = true } } - override fun createToken(request: CVVTokenRequest) { - // TODO; work in progress + override fun createToken(resultHandler: (CVVTokenizationResultHandler) -> Unit) { + val internalCVVTokenRequest = InternalCVVTokenRequest( + cvv = cvvInputFieldTextValue.value, + cardScheme = cvvComponentConfig.cardScheme, + resultHandler = resultHandler + ) + + cvvTokenizationUseCase.execute(internalCVVTokenRequest) } override fun provideCvvComponentContent( @@ -58,4 +64,9 @@ internal class InternalCVVComponentMediator( internal fun setIsCVVComponentCalled(shouldCVVComponentCall: Boolean) { isCVVComponentCalled.value = shouldCVVComponentCall } + + @VisibleForTesting + internal fun setCVVInputFieldTextValue(onValueChange: String) { + cvvInputFieldTextValue.value = onValueChange + } } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/models/InternalCVVTokenRequest.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/models/InternalCVVTokenRequest.kt new file mode 100644 index 000000000..2142dae0b --- /dev/null +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/models/InternalCVVTokenRequest.kt @@ -0,0 +1,10 @@ +package com.checkout.frames.cvvinputfield.models + +import com.checkout.base.model.CardScheme +import com.checkout.tokenization.model.CVVTokenizationResultHandler + +internal data class InternalCVVTokenRequest( + val cvv: String, + val cardScheme: CardScheme = CardScheme.UNKNOWN, + val resultHandler: (CVVTokenizationResultHandler) -> Unit, +) diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCase.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCase.kt new file mode 100644 index 000000000..1aec7618c --- /dev/null +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCase.kt @@ -0,0 +1,15 @@ +package com.checkout.frames.cvvinputfield.usecase + +import com.checkout.api.CheckoutApiService +import com.checkout.base.usecase.UseCase +import com.checkout.frames.cvvinputfield.models.InternalCVVTokenRequest +import com.checkout.tokenization.model.CVVTokenizationRequest + +internal class CVVTokenizationUseCase( + private val checkoutApiService: CheckoutApiService, +) : UseCase { + + override fun execute(data: InternalCVVTokenRequest) = checkoutApiService.createToken( + CVVTokenizationRequest(cvv = data.cvv, cardScheme = data.cardScheme, resultHandler = data.resultHandler) + ) +} diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModel.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModel.kt index 8e6a3db5c..1029db902 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModel.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModel.kt @@ -35,14 +35,7 @@ internal class CVVInputFieldViewModel internal constructor( private fun validate() { with(cvvComponentConfig) { when (cvvComponentValidator.validate(cvvInputFieldState.cvv.value, cardScheme)) { - is ValidationResult.Success -> { - if (cvvInputFieldState.cvv.value.isNotEmpty()) { - onCVVValueChange(true) - } else { - onCVVValueChange(false) - } - } - + is ValidationResult.Success -> onCVVValueChange(true) is ValidationResult.Failure -> onCVVValueChange(false) } } diff --git a/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentApiTest.kt b/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentApiTest.kt index 12b17d35b..c3800bbf0 100644 --- a/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentApiTest.kt +++ b/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentApiTest.kt @@ -2,37 +2,45 @@ package com.checkout.frames.cvvinputfield import android.content.Context import com.checkout.base.model.Environment -import com.checkout.frames.cvvinputfield.api.CVVComponentApi import com.checkout.frames.cvvinputfield.api.InternalCVVComponentApi import com.checkout.frames.cvvinputfield.api.InternalCVVComponentMediator import com.checkout.frames.cvvinputfield.models.CVVComponentConfig -import io.mockk.mockk +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.spyk import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +@ExtendWith(MockKExtension::class) internal class InternalCVVComponentApiTest { private lateinit var publicKey: String private lateinit var environment: Environment - private lateinit var context: Context + + @RelaxedMockK + private lateinit var mockContext: Context + + @RelaxedMockK private lateinit var cvvComponentConfig: CVVComponentConfig @BeforeEach fun setUp() { - // Initialize mock instances for parameters publicKey = "test_key" - environment = Environment.SANDBOX // or any desired environment - context = mockk() - cvvComponentConfig = mockk() + environment = Environment.SANDBOX } @Test fun `when createComponentMediator is invoked then InternalCVVComponentMediator is correctly created`() { // Given - val cvvComponentApi: CVVComponentApi = InternalCVVComponentApi(publicKey, environment, context) + val internalCVVComponentApi = spyk( + InternalCVVComponentApi( + publicKey = "your_public_key", environment = Environment.SANDBOX, context = mockContext + ) + ) // When - val componentMediator = cvvComponentApi.createComponentMediator(cvvComponentConfig) + val componentMediator = internalCVVComponentApi.createComponentMediator(cvvComponentConfig) // Then assertEquals(InternalCVVComponentMediator::class.java, componentMediator.javaClass) diff --git a/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt b/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt new file mode 100644 index 000000000..600aa0e7f --- /dev/null +++ b/frames/src/test/java/com/checkout/frames/cvvinputfield/InternalCVVComponentMediatorTest.kt @@ -0,0 +1,51 @@ +package com.checkout.frames.cvvinputfield + +import com.checkout.base.model.CardScheme +import com.checkout.base.usecase.UseCase +import com.checkout.frames.cvvinputfield.api.InternalCVVComponentMediator +import com.checkout.frames.cvvinputfield.models.CVVComponentConfig +import com.checkout.frames.cvvinputfield.models.InternalCVVTokenRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class InternalCVVComponentMediatorTest { + @MockK + private lateinit var mockCVVTokenizationUseCase: UseCase + + @MockK + private lateinit var mockCVVComponentConfig: CVVComponentConfig + + private lateinit var internalCVVComponentMediator: InternalCVVComponentMediator + + @BeforeEach + fun setUp() { + every { mockCVVComponentConfig.cardScheme } returns CardScheme.VISA + every { mockCVVTokenizationUseCase.execute(any()) } returns Unit + internalCVVComponentMediator = InternalCVVComponentMediator(mockCVVComponentConfig, mockCVVTokenizationUseCase) + } + + @Test + fun `when createToken is invoked then CVVTokenizationUseCase is executed with correct request`() { + // Given + val inputCVVValue = "123" + internalCVVComponentMediator.setCVVInputFieldTextValue(inputCVVValue) + val mockResultHandler: (CVVTokenizationResultHandler) -> Unit = mockk() + val internalCVVTokenRequest = InternalCVVTokenRequest(inputCVVValue, CardScheme.VISA, mockResultHandler) + + // When + internalCVVComponentMediator.createToken(mockResultHandler) + + // Then + verify(exactly = 1) { + mockCVVTokenizationUseCase.execute(eq(internalCVVTokenRequest)) + } + } +} diff --git a/frames/src/test/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCaseTest.kt b/frames/src/test/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCaseTest.kt new file mode 100644 index 000000000..b1760139c --- /dev/null +++ b/frames/src/test/java/com/checkout/frames/cvvinputfield/usecase/CVVTokenizationUseCaseTest.kt @@ -0,0 +1,51 @@ +package com.checkout.frames.cvvinputfield.usecase + +import com.checkout.api.CheckoutApiService +import com.checkout.base.model.CardScheme +import com.checkout.frames.cvvinputfield.models.InternalCVVTokenRequest +import com.checkout.tokenization.model.CVVTokenizationRequest +import com.checkout.tokenization.model.CVVTokenizationResultHandler +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import io.mockk.verify + +@ExtendWith(MockKExtension::class) +internal class CVVTokenizationUseCaseTest { + + @RelaxedMockK + private lateinit var mockCheckoutApiService: CheckoutApiService + + private lateinit var cvvTokenizationUseCase: CVVTokenizationUseCase + + @BeforeEach + fun setUp() { + val mockCVVTokenizationRequest = mockk() + every { mockCheckoutApiService.createToken(mockCVVTokenizationRequest) } just runs + + cvvTokenizationUseCase = CVVTokenizationUseCase(mockCheckoutApiService) + } + + @Test + fun `execute should call createToken with correct parameters`() { + // Given + val cvv = "123" + val cardScheme = CardScheme.VISA + val resultHandler: (CVVTokenizationResultHandler) -> Unit = {} + + val requestData = InternalCVVTokenRequest(cvv, cardScheme, resultHandler) + + // When + cvvTokenizationUseCase.execute(requestData) + + // Then + val expectedRequest = CVVTokenizationRequest(cvv, cardScheme, resultHandler) + verify(exactly = 1) { mockCheckoutApiService.createToken(expectedRequest) } + } +} diff --git a/frames/src/test/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModelTest.kt b/frames/src/test/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModelTest.kt index 655a82bb2..c398a0a16 100644 --- a/frames/src/test/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModelTest.kt +++ b/frames/src/test/java/com/checkout/frames/cvvinputfield/viewmodel/CVVInputFieldViewModelTest.kt @@ -201,7 +201,7 @@ internal class CVVInputFieldViewModelTest { every { cvvComponentConfig.cardScheme } returns CardScheme.MAESTRO every { mockCVVComponentValidator.validate(eq(validCVV), eq(testCardScheme)) - } returns ValidationResult.Success(Unit) + } returns ValidationResult.Failure(CheckoutError("Test Error code")) // When viewModel.onCvvChange(validCVV)