From 745001fc5d5a40271938e2d3c1ec1e8d13789381 Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 09:34:58 +0000 Subject: [PATCH 1/7] [PIMOB-5427] Update Risk to 3.0.1 and require API 23 --- README.md | 2 +- buildSrc/src/main/java/ProjectConfig.kt | 2 +- buildSrc/src/main/java/Versions.kt | 2 +- checkout/consumer-rules.pro | 9 ++++++++- .../checkout/threedsecure/utils/WebResourcesExtension.kt | 6 +----- .../threedsecure/webview/ThreeDSWebViewClientTest.kt | 3 --- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 07580bb78..d7da2991d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Start accepting online payments in just a few minutes with our Android SDK. It's ## Requirements -- Android minimum SDK 21 +- Android minimum SDK 23 > Compatibility verified with `targetSdk` versions 21 to 34 diff --git a/buildSrc/src/main/java/ProjectConfig.kt b/buildSrc/src/main/java/ProjectConfig.kt index b3463b2c4..44be6c896 100644 --- a/buildSrc/src/main/java/ProjectConfig.kt +++ b/buildSrc/src/main/java/ProjectConfig.kt @@ -2,7 +2,7 @@ object AndroidConfig { const val compileSdk = 34 - const val minSdk = 21 + const val minSdk = 23 const val targetSdk = 34 const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner" diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 62eb2cc51..5e924f019 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -33,7 +33,7 @@ object Versions { const val moshi = "1.15.1" // Risk SDK Dependencies - const val riskSdk = "2.1.0" + const val riskSdk = "3.0.1" // Unit Testing Dependencies const val junit5Jupiter = "5.13.4" diff --git a/checkout/consumer-rules.pro b/checkout/consumer-rules.pro index 77097b6b3..2476b3b3a 100644 --- a/checkout/consumer-rules.pro +++ b/checkout/consumer-rules.pro @@ -1 +1,8 @@ --keep class com.checkout.** { *; } \ No newline at end of file +-keep class com.checkout.** { *; } + +# Risk SDK leaves Fingerprint's optional device-location collection disabled. +-dontwarn com.google.android.gms.location.** +-dontwarn com.google.android.gms.tasks.CancellationToken +-dontwarn com.google.android.gms.tasks.OnFailureListener +-dontwarn com.google.android.gms.tasks.OnSuccessListener +-dontwarn com.google.android.gms.tasks.Task \ No newline at end of file diff --git a/checkout/src/main/java/com/checkout/threedsecure/utils/WebResourcesExtension.kt b/checkout/src/main/java/com/checkout/threedsecure/utils/WebResourcesExtension.kt index cbe041865..235c6cbc4 100644 --- a/checkout/src/main/java/com/checkout/threedsecure/utils/WebResourcesExtension.kt +++ b/checkout/src/main/java/com/checkout/threedsecure/utils/WebResourcesExtension.kt @@ -1,14 +1,10 @@ package com.checkout.threedsecure.utils -import android.os.Build import android.webkit.WebResourceError import android.webkit.WebResourceResponse import com.checkout.threedsecure.error.ThreeDSError internal fun WebResourceResponse.toThreeDSError() = ThreeDSError(this.statusCode.toString(), this.reasonPhrase) -internal fun WebResourceError.toThreeDSError() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { +internal fun WebResourceError.toThreeDSError() = ThreeDSError(this.errorCode.toString(), this.description.toString()) -} else { - null -} diff --git a/checkout/src/test/java/com/checkout/threedsecure/webview/ThreeDSWebViewClientTest.kt b/checkout/src/test/java/com/checkout/threedsecure/webview/ThreeDSWebViewClientTest.kt index 09d9125a6..9568f8ac0 100644 --- a/checkout/src/test/java/com/checkout/threedsecure/webview/ThreeDSWebViewClientTest.kt +++ b/checkout/src/test/java/com/checkout/threedsecure/webview/ThreeDSWebViewClientTest.kt @@ -1,8 +1,6 @@ package com.checkout.threedsecure.webview -import android.os.Build import android.webkit.WebViewClient -import androidx.test.filters.SdkSuppress import com.checkout.threedsecure.logging.ThreeDSLogger import io.mockk.MockKAnnotations import io.mockk.impl.annotations.RelaxedMockK @@ -38,7 +36,6 @@ internal class ThreeDSWebViewClientTest { verify(exactly = 1) { logger.logPresentedEvent() } } - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) @Test fun `when page committed visible for the first time then log loaded event`() { // When From ee32586a42afccc2d7f8ee68f0682de2ca57169e Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 09:38:08 +0000 Subject: [PATCH 2/7] [PIMOB-5427] Share prefix validation across token and Risk endpoints --- .../com/checkout/BaseUrlPrefixValidator.kt | 8 +++ .../com/checkout/CheckoutApiServiceFactory.kt | 1 + .../logging/utils/EnvironmentExtension.kt | 8 +-- .../usecase/RiskInstanceProvider.kt | 3 + .../tokenization/usecase/RiskSdkUseCase.kt | 3 +- .../com/checkout/EnvironmentExtensionTest.kt | 21 ++++++- .../tokenization/RiskInstanceProviderTest.kt | 63 +++++++++++++++++++ .../tokenization/RiskSdkUseCaseTest.kt | 7 ++- .../cvvinputfield/CVVComponentApiFactory.kt | 4 +- .../paymentform/model/PaymentFormConfig.kt | 4 +- 10 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 checkout/src/main/java/com/checkout/BaseUrlPrefixValidator.kt create mode 100644 checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt diff --git a/checkout/src/main/java/com/checkout/BaseUrlPrefixValidator.kt b/checkout/src/main/java/com/checkout/BaseUrlPrefixValidator.kt new file mode 100644 index 000000000..a084ab761 --- /dev/null +++ b/checkout/src/main/java/com/checkout/BaseUrlPrefixValidator.kt @@ -0,0 +1,8 @@ +package com.checkout + +internal object BaseUrlPrefixValidator { + fun validPrefixOrNull(baseUrlPrefix: String?): String? = + baseUrlPrefix?.takeIf { LABEL_REGEX.matches(it) } + + private val LABEL_REGEX = Regex("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") +} diff --git a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt index 3f6d05054..fccfc0005 100644 --- a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt +++ b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt @@ -86,6 +86,7 @@ public object CheckoutApiServiceFactory { publicKey, riskSDKFramesOptions, RiskInstanceProvider, + baseUrlPrefix, ), ) diff --git a/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt b/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt index b2cd994cf..e25f3cb8b 100644 --- a/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt +++ b/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt @@ -1,5 +1,6 @@ package com.checkout.logging.utils +import com.checkout.BaseUrlPrefixValidator import com.checkout.base.model.Environment import com.checkout.base.util.EnvironmentConstants.PRODUCTION_LOGGING import com.checkout.base.util.EnvironmentConstants.PRODUCTION_SERVER_URL @@ -23,11 +24,6 @@ internal fun Environment.toLoggingName() = when (this) { } private fun String.applyPrefix(prefix: String?): String { - val validatedPrefix = prefix?.baseUrlPrefixValidator() ?: return this + val validatedPrefix = BaseUrlPrefixValidator.validPrefixOrNull(prefix) ?: return this return this.replace(HTTPS_PROTOCOL, "$HTTPS_PROTOCOL$validatedPrefix.") } - -private fun String.baseUrlPrefixValidator() = this - .takeIf { prefix -> - prefix.isNotEmpty() && prefix.all { char -> char in 'a'..'z' || char in 'A'..'Z' || char in '0'..'9' } - } diff --git a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt index 42aa0d5b1..475f22627 100644 --- a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt +++ b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt @@ -1,6 +1,7 @@ package com.checkout.tokenization.usecase import android.content.Context +import com.checkout.BaseUrlPrefixValidator import com.checkout.base.model.Environment import com.checkout.risk.FramesOptions import com.checkout.risk.Risk @@ -15,6 +16,7 @@ internal object RiskInstanceProvider { publicKey: String, environment: Environment, framesOptions: FramesOptions, + baseUrlPrefix: String?, ): Risk? { if (riskInstance != null) { return riskInstance @@ -33,6 +35,7 @@ internal object RiskInstanceProvider { publicKey = publicKey, environment = riskEnvironment, framesOptions = framesOptions, + mssd = BaseUrlPrefixValidator.validPrefixOrNull(baseUrlPrefix), ), ) return riskInstance diff --git a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskSdkUseCase.kt b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskSdkUseCase.kt index ccf0dac02..6d803ad94 100644 --- a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskSdkUseCase.kt +++ b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskSdkUseCase.kt @@ -11,9 +11,10 @@ internal class RiskSdkUseCase( private val publicKey: String, private val framesOptions: FramesOptions, private val riskInstanceProvider: RiskInstanceProvider, + private val baseUrlPrefix: String? = null, ) { suspend fun execute(data: TokenResult) { - val riskInstance = riskInstanceProvider.provide(context, publicKey, environment, framesOptions) + val riskInstance = riskInstanceProvider.provide(context, publicKey, environment, framesOptions, baseUrlPrefix) when (data) { is TokenResult.Success -> riskInstance?.publishData(cardToken = data.result) is TokenResult.Failure -> {} diff --git a/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt b/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt index 1c78f4625..d950729ee 100644 --- a/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt +++ b/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt @@ -33,6 +33,14 @@ internal class EnvironmentExtensionTest { assertEquals("https://mySubdomain123.api.checkout.com/tokens", result) } + @Test + fun `valid subdomain labels route both environments to prefixed hosts`() { + listOf("a", "0", "Ab", "merchant-123", "a--b", "A" + "-".repeat(61) + "9").forEach { prefix -> + assertEquals("https://$prefix.api.checkout.com/tokens", Environment.PRODUCTION.toBaseUrl(prefix)) + assertEquals("https://$prefix.api.sandbox.checkout.com/tokens", Environment.SANDBOX.toBaseUrl(prefix)) + } + } + @Test fun `when toBaseUrl SANDBOX with null subDomainPrefix returns sandbox server URL`() { val result = Environment.SANDBOX.toBaseUrl(null) @@ -64,10 +72,18 @@ internal class EnvironmentExtensionTest { } @Test - fun `toBaseUrl PRODUCTION returns default URL for any non-alphanumeric prefix`() { + fun `invalid subdomain labels use default hosts in both environments`() { val invalidPrefixList = listOf( + null, + "", + " ", + "-", + "-prefix", + "prefix-", + "é", + "a\n", + "a".repeat(64), "invalid_prefix", - "invalid-prefix", "invalid.prefix", "invalid@prefix", "invalid!prefix", @@ -88,6 +104,7 @@ internal class EnvironmentExtensionTest { PRODUCTION_SERVER_URL, result, ) + assertEquals(EnvironmentConstants.SANDBOX_SERVER_URL, Environment.SANDBOX.toBaseUrl(invalidPrefix)) } } } diff --git a/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt b/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt new file mode 100644 index 000000000..498d44be2 --- /dev/null +++ b/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt @@ -0,0 +1,63 @@ +package com.checkout.tokenization + +import android.content.Context +import com.checkout.base.model.Environment +import com.checkout.risk.FramesOptions +import com.checkout.risk.Risk +import com.checkout.risk.RiskConfig +import com.checkout.risk.RiskEnvironment +import com.checkout.tokenization.usecase.RiskInstanceProvider +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class RiskInstanceProviderTest { + private val context: Context = mockk() + private val framesOptions = FramesOptions("1.0.0", "productIdentifier", "correlationId") + private val riskConfigs = mutableListOf() + + @BeforeEach + fun setup() { + mockkObject(Risk.Companion) + coEvery { Risk.getInstance(context, capture(riskConfigs)) } returns null + } + + @AfterEach + fun tearDown() { + unmockkObject(Risk.Companion) + } + + @Test + fun `valid subdomain labels are forwarded to Risk in both environments`() = runTest { + listOf("a", "0", "Ab", "merchant-123", "a--b", "A" + "-".repeat(61) + "9").forEach { prefix -> + provideInBothEnvironments(prefix) + assertEquals(prefix, riskConfigs[riskConfigs.lastIndex - 1].mssd) + assertEquals(prefix, riskConfigs.last().mssd) + } + } + + @Test + fun `null empty and invalid prefixes use default Risk hosts`() = runTest { + listOf(null, "", " ", "-", "-merchant", "merchant-", "a.b", "é", "a\n", "a".repeat(64)).forEach { prefix -> + provideInBothEnvironments(prefix) + assertEquals(null, riskConfigs[riskConfigs.lastIndex - 1].mssd) + assertEquals(null, riskConfigs.last().mssd) + } + } + + private suspend fun provideInBothEnvironments(prefix: String?) { + listOf(Environment.SANDBOX, Environment.PRODUCTION).forEach { environment -> + RiskInstanceProvider.provide(context, "publicKey", environment, framesOptions, prefix) + } + assertEquals(RiskEnvironment.SANDBOX, riskConfigs[riskConfigs.lastIndex - 1].environment) + assertEquals(RiskEnvironment.PRODUCTION, riskConfigs.last().environment) + assertEquals("publicKey", riskConfigs.last().publicKey) + assertEquals(framesOptions, riskConfigs.last().framesOptions) + } +} diff --git a/checkout/src/test/java/com/checkout/tokenization/RiskSdkUseCaseTest.kt b/checkout/src/test/java/com/checkout/tokenization/RiskSdkUseCaseTest.kt index a3b785504..a03d39bd5 100644 --- a/checkout/src/test/java/com/checkout/tokenization/RiskSdkUseCaseTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/RiskSdkUseCaseTest.kt @@ -32,7 +32,9 @@ public class RiskSdkUseCaseTest { @BeforeEach public fun setup() { coEvery { tokenDetails.token } returns TOKEN - coEvery { riskInstanceProvider.provide(context, PUBLIC_KEY, environment, framesOptions) } returns riskInstance + coEvery { + riskInstanceProvider.provide(context, PUBLIC_KEY, environment, framesOptions, "mssd") + } returns riskInstance coEvery { riskInstance.publishData(any()) } returns mockk() useCase = RiskSdkUseCase( @@ -41,6 +43,7 @@ public class RiskSdkUseCaseTest { publicKey = PUBLIC_KEY, riskInstanceProvider = riskInstanceProvider, framesOptions = framesOptions, + baseUrlPrefix = "mssd", ) } @@ -48,7 +51,7 @@ public class RiskSdkUseCaseTest { public fun `Success result should trigger publishData`() { runBlocking { useCase.execute(TokenResult.Success(tokenDetails.token)) - coVerify { riskInstanceProvider.provide(context, PUBLIC_KEY, environment, framesOptions) } + coVerify { riskInstanceProvider.provide(context, PUBLIC_KEY, environment, framesOptions, "mssd") } coVerify { riskInstance.publishData(TOKEN) } } } diff --git a/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVComponentApiFactory.kt b/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVComponentApiFactory.kt index f56a372be..560684a9a 100644 --- a/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVComponentApiFactory.kt +++ b/frames/src/main/java/com/checkout/frames/cvvinputfield/CVVComponentApiFactory.kt @@ -16,7 +16,9 @@ public object CVVComponentApiFactory { * @param publicKey - used for client-side authentication in the SDK * @param environment - [Environment] represent the environment for tokenization * @param context - represent the application context - * @param baseUrlPrefix - an optional alphanumeric prefix used to route requests to a specific regional or merchant-specific subdomain (e.g., "msdd"). Must be alphanumeric. + * @param baseUrlPrefix - an optional merchant-specific subdomain label (e.g., "mssd"). + * Must contain 1 to 63 ASCII letters, digits, or hyphens, starting and ending with a letter or digit. + * Null, empty, or invalid values are ignored and use the default hosts. */ @JvmStatic public fun create( diff --git a/frames/src/main/java/com/checkout/frames/screen/paymentform/model/PaymentFormConfig.kt b/frames/src/main/java/com/checkout/frames/screen/paymentform/model/PaymentFormConfig.kt index cec579d7c..f4c7732f6 100644 --- a/frames/src/main/java/com/checkout/frames/screen/paymentform/model/PaymentFormConfig.kt +++ b/frames/src/main/java/com/checkout/frames/screen/paymentform/model/PaymentFormConfig.kt @@ -14,7 +14,9 @@ import com.checkout.frames.style.screen.PaymentFormStyle * @param paymentFlowHandler - [PaymentFlowHandler] represent the handler for PaymentForm * @param supportedCardSchemeList - represent the supported card schemes [CardScheme] in PaymentForm * @param prefillData - [PrefillData] represent the data for prefill in the PaymentForm - * @param baseUrlPrefix - an optional alphanumeric prefix used to route requests to a specific regional or merchant-specific subdomain (e.g., "msdd"). Must be alphanumeric. + * @param baseUrlPrefix - an optional merchant-specific subdomain label (e.g., "mssd"). + * Must contain 1 to 63 ASCII letters, digits, or hyphens, starting and ending with a letter or digit. + * Null, empty, or invalid values are ignored and use the default hosts. */ public data class PaymentFormConfig @JvmOverloads constructor( public val publicKey: String, From 51888e9589cf36c94899f59d23547a77d7c63091 Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Wed, 11 Nov 2026 13:23:44 +0000 Subject: [PATCH 3/7] [PIMOB-5427] Fix coroutine dispatcher race in token repository tests --- .../repository/TokenRepositoryImplTest.kt | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) 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 b898cc060..aa75dbfc0 100644 --- a/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt @@ -44,8 +44,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested @@ -77,18 +75,7 @@ internal class TokenRepositoryImplTest { @BeforeEach fun setUp() { Dispatchers.setMain(testDispatcher) - tokenRepositoryImpl = TokenRepositoryImpl( - networkApiClient = mockTokenNetworkApiClient, - cardToTokenRequestMapper = CardToTokenRequestMapper(), - cvvToTokenNetworkRequestMapper = CVVToTokenNetworkRequestMapper(), - cardTokenizationNetworkDataMapper = CardTokenizationNetworkDataMapper(), - validateTokenizationDataUseCase = mockValidateTokenizationDataUseCase, - validateCVVTokenizationDataUseCase = mockValidateCVVTokenizationDataUseCase, - logger = mockTokenizationLogger, - publicKey = "test_key", - cvvTokenizationNetworkDataMapper = CVVTokenizationNetworkDataMapper(), - riskSdkUseCase = mockRiskSdkUseCase, - ) + tokenRepositoryImpl = createTokenRepository(testDispatcher) } @AfterEach @@ -111,7 +98,7 @@ internal class TokenRepositoryImplTest { fun `when sendCardTokenRequest invoked with network error response then failure handler invoked`() { testCardTokenResultInvocation( false, - NetworkApiResponse.NetworkError(NullPointerException()), + NetworkApiResponse.NetworkError(NullPointerException("Network connection lost")), ) } @@ -184,7 +171,7 @@ internal class TokenRepositoryImplTest { response: NetworkApiResponse, ) = runTest { // Given - var isSuccess = false + var isSuccess: Boolean? = null tokenRepositoryImpl.networkCoroutineScope = CoroutineScope(testDispatcher) @@ -204,13 +191,7 @@ internal class TokenRepositoryImplTest { ) advanceUntilIdle() // Then - launch { - if (successHandlerInvoked) { - assertTrue(isSuccess) - } else { - assertFalse(isSuccess) - } - } + assertEquals(successHandlerInvoked, isSuccess) } private fun testCardTokenEventInvocation(isSuccessResponse: Boolean) = From 702990d4bb745c055771418d21de951f82ea6ac9 Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 09:42:37 +0000 Subject: [PATCH 4/7] [PIMOB-5427] Remove stale SDK compatibility claim --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d7da2991d..a044959a3 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ Start accepting online payments in just a few minutes with our Android SDK. It's ## Requirements - Android minimum SDK 23 -> Compatibility verified with `targetSdk` versions 21 to 34 - ## Documentation From 1bd30faccf54f6509650bf88b0b07d6596cd71ed Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 10:16:05 +0000 Subject: [PATCH 5/7] [PIMOB-5427] Limit test fix to dispatcher injection --- .../repository/TokenRepositoryImplTest.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 aa75dbfc0..73670bb01 100644 --- a/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/repository/TokenRepositoryImplTest.kt @@ -44,6 +44,8 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested @@ -98,7 +100,7 @@ internal class TokenRepositoryImplTest { fun `when sendCardTokenRequest invoked with network error response then failure handler invoked`() { testCardTokenResultInvocation( false, - NetworkApiResponse.NetworkError(NullPointerException("Network connection lost")), + NetworkApiResponse.NetworkError(NullPointerException()), ) } @@ -171,7 +173,7 @@ internal class TokenRepositoryImplTest { response: NetworkApiResponse, ) = runTest { // Given - var isSuccess: Boolean? = null + var isSuccess = false tokenRepositoryImpl.networkCoroutineScope = CoroutineScope(testDispatcher) @@ -191,7 +193,13 @@ internal class TokenRepositoryImplTest { ) advanceUntilIdle() // Then - assertEquals(successHandlerInvoked, isSuccess) + launch { + if (successHandlerInvoked) { + assertTrue(isSuccess) + } else { + assertFalse(isSuccess) + } + } } private fun testCardTokenEventInvocation(isSuccessResponse: Boolean) = From 90689ca4e595f40880d4e4a72c48d8bf7c0b07e8 Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 10:37:28 +0000 Subject: [PATCH 6/7] [PIMOB-5427] Centralize baseUrlPrefix validation in the API factory --- .../com/checkout/CheckoutApiServiceFactory.kt | 3 +- .../logging/utils/EnvironmentExtension.kt | 5 +-- .../usecase/RiskInstanceProvider.kt | 3 +- .../checkout/CheckoutApiServiceFactoryTest.kt | 29 ++++++++++++ .../com/checkout/EnvironmentExtensionTest.kt | 44 ------------------- .../tokenization/RiskInstanceProviderTest.kt | 10 ++--- 6 files changed, 38 insertions(+), 56 deletions(-) diff --git a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt index fccfc0005..9fe9d877b 100644 --- a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt +++ b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt @@ -43,6 +43,7 @@ public object CheckoutApiServiceFactory { context: Context, baseUrlPrefix: String? = null, ): CheckoutApiService { + val validatedBaseUrlPrefix = BaseUrlPrefixValidator.validPrefixOrNull(baseUrlPrefix) val logger = EventLoggerProvider.provide() logger.setup(context, environment) correlationId = logger.correlationId @@ -53,7 +54,7 @@ public object CheckoutApiServiceFactory { ) return CheckoutApiClient( - provideTokenRepository(context, publicKey, environment, baseUrlPrefix), + provideTokenRepository(context, publicKey, environment, validatedBaseUrlPrefix), provideThreeDSExecutor(logger), ) } diff --git a/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt b/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt index e25f3cb8b..04ff7cc63 100644 --- a/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt +++ b/checkout/src/main/java/com/checkout/logging/utils/EnvironmentExtension.kt @@ -1,6 +1,5 @@ package com.checkout.logging.utils -import com.checkout.BaseUrlPrefixValidator import com.checkout.base.model.Environment import com.checkout.base.util.EnvironmentConstants.PRODUCTION_LOGGING import com.checkout.base.util.EnvironmentConstants.PRODUCTION_SERVER_URL @@ -24,6 +23,6 @@ internal fun Environment.toLoggingName() = when (this) { } private fun String.applyPrefix(prefix: String?): String { - val validatedPrefix = BaseUrlPrefixValidator.validPrefixOrNull(prefix) ?: return this - return this.replace(HTTPS_PROTOCOL, "$HTTPS_PROTOCOL$validatedPrefix.") + if (prefix == null) return this + return this.replace(HTTPS_PROTOCOL, "$HTTPS_PROTOCOL$prefix.") } diff --git a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt index 475f22627..65f0a65cf 100644 --- a/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt +++ b/checkout/src/main/java/com/checkout/tokenization/usecase/RiskInstanceProvider.kt @@ -1,7 +1,6 @@ package com.checkout.tokenization.usecase import android.content.Context -import com.checkout.BaseUrlPrefixValidator import com.checkout.base.model.Environment import com.checkout.risk.FramesOptions import com.checkout.risk.Risk @@ -35,7 +34,7 @@ internal object RiskInstanceProvider { publicKey = publicKey, environment = riskEnvironment, framesOptions = framesOptions, - mssd = BaseUrlPrefixValidator.validPrefixOrNull(baseUrlPrefix), + mssd = baseUrlPrefix, ), ) return riskInstance diff --git a/checkout/src/test/java/com/checkout/CheckoutApiServiceFactoryTest.kt b/checkout/src/test/java/com/checkout/CheckoutApiServiceFactoryTest.kt index 1db5f878b..17eabb093 100644 --- a/checkout/src/test/java/com/checkout/CheckoutApiServiceFactoryTest.kt +++ b/checkout/src/test/java/com/checkout/CheckoutApiServiceFactoryTest.kt @@ -5,12 +5,17 @@ import com.checkout.base.model.Environment import com.checkout.logging.EventLoggerProvider import com.checkout.logging.Logger import com.checkout.logging.model.LoggingEvent +import com.checkout.logging.utils.toBaseUrl import io.mockk.every import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkObject +import io.mockk.unmockkStatic import io.mockk.verify +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -24,9 +29,33 @@ internal class CheckoutApiServiceFactoryTest { @BeforeEach fun setUp() { mockkObject(EventLoggerProvider) + mockkStatic(Environment::toBaseUrl) every { EventLoggerProvider.provide() } returns mockLogger } + @AfterEach + fun tearDown() { + unmockkObject(EventLoggerProvider) + unmockkStatic(Environment::toBaseUrl) + } + + @Test + fun `valid base URL prefixes are preserved during client creation`() { + listOf("a", "0", "Ab", "merchant-123", "a--b", "A" + "-".repeat(61) + "9").forEach { prefix -> + CheckoutApiServiceFactory.create("", Environment.SANDBOX, mockk(), prefix) + verify(exactly = 1) { Environment.SANDBOX.toBaseUrl(prefix) } + } + } + + @Test + fun `null empty and invalid base URL prefixes are normalized to null during client creation`() { + val prefixes = listOf(null, "", " ", "-", "-merchant", "merchant-", "a.b", "a_b", "é", "a\n", "a".repeat(64)) + prefixes.forEach { prefix -> + CheckoutApiServiceFactory.create("", Environment.SANDBOX, mockk(), prefix) + } + verify(exactly = prefixes.size) { Environment.SANDBOX.toBaseUrl(null) } + } + @Test fun `when checkout API service is created then logger is set up`() { // Given diff --git a/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt b/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt index d950729ee..b0f6ca544 100644 --- a/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt +++ b/checkout/src/test/java/com/checkout/EnvironmentExtensionTest.kt @@ -2,7 +2,6 @@ package com.checkout import com.checkout.base.model.Environment import com.checkout.base.util.EnvironmentConstants -import com.checkout.base.util.EnvironmentConstants.PRODUCTION_SERVER_URL import com.checkout.logging.utils.toBaseUrl import org.junit.Assert.assertEquals import org.junit.Test @@ -64,47 +63,4 @@ internal class EnvironmentExtensionTest { val result = Environment.SANDBOX.toBaseUrl("test99") assertEquals("https://test99.api.sandbox.checkout.com/tokens", result) } - - @Test - fun `when toBaseUrl with empty string subDomainPrefix uses default URL for production`() { - val result = Environment.PRODUCTION.toBaseUrl("") - assertEquals("https://api.checkout.com/tokens", result) - } - - @Test - fun `invalid subdomain labels use default hosts in both environments`() { - val invalidPrefixList = listOf( - null, - "", - " ", - "-", - "-prefix", - "prefix-", - "é", - "a\n", - "a".repeat(64), - "invalid_prefix", - "invalid.prefix", - "invalid@prefix", - "invalid!prefix", - "invalid#prefix", - "invalid\$prefix", - "invalid%prefix", - "invalid^prefix", - "invalid&prefix", - "invalid*prefix", - "invalid(prefix", - "invalid)prefix", - "invalid prefix", - ) - - invalidPrefixList.forEach { invalidPrefix -> - val result = Environment.PRODUCTION.toBaseUrl(invalidPrefix) - assertEquals( - PRODUCTION_SERVER_URL, - result, - ) - assertEquals(EnvironmentConstants.SANDBOX_SERVER_URL, Environment.SANDBOX.toBaseUrl(invalidPrefix)) - } - } } diff --git a/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt b/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt index 498d44be2..ad0ed3f42 100644 --- a/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt +++ b/checkout/src/test/java/com/checkout/tokenization/RiskInstanceProviderTest.kt @@ -43,12 +43,10 @@ internal class RiskInstanceProviderTest { } @Test - fun `null empty and invalid prefixes use default Risk hosts`() = runTest { - listOf(null, "", " ", "-", "-merchant", "merchant-", "a.b", "é", "a\n", "a".repeat(64)).forEach { prefix -> - provideInBothEnvironments(prefix) - assertEquals(null, riskConfigs[riskConfigs.lastIndex - 1].mssd) - assertEquals(null, riskConfigs.last().mssd) - } + fun `null prefix is forwarded to Risk in both environments`() = runTest { + provideInBothEnvironments(null) + assertEquals(null, riskConfigs[riskConfigs.lastIndex - 1].mssd) + assertEquals(null, riskConfigs.last().mssd) } private suspend fun provideInBothEnvironments(prefix: String?) { From b7dd93ea177e077daa268fc6b95fc9701f268363 Mon Sep 17 00:00:00 2001 From: Adrian Orlowski Date: Thu, 12 Nov 2026 10:52:10 +0000 Subject: [PATCH 7/7] [PIMOB-5427] added KDoc --- .../java/com/checkout/CheckoutApiServiceFactory.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt index 9fe9d877b..77ea52a43 100644 --- a/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt +++ b/checkout/src/main/java/com/checkout/CheckoutApiServiceFactory.kt @@ -36,6 +36,16 @@ public object CheckoutApiServiceFactory { private lateinit var correlationId: String private lateinit var riskSDKFramesOptions: FramesOptions + /** + * Creates [CheckoutApiService]. + * + * @param publicKey the public API key used for client-side authentication. + * @param environment the environment for tokenization. + * @param context the Android context. + * @param baseUrlPrefix an optional merchant-specific subdomain label (e.g., "mssd"), not a URL or hostname. + * Must contain 1 to 63 ASCII letters, digits, or hyphens, starting and ending with a letter or digit. + * Applied to both tokenization and Risk requests. Null, empty, or invalid values use the default hosts. + */ @JvmStatic public fun create( publicKey: String,