Skip to content

Commit 68175c3

Browse files
committed
Use ConcurrentHashMap for the PaymentMethodProvider to support thread safety
COSDK-550
1 parent 82f2bf2 commit 68175c3

File tree

3 files changed

+198
-1
lines changed

3 files changed

+198
-1
lines changed

core/src/main/java/com/adyen/checkout/core/components/internal/PaymentMethodProvider.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@
99
package com.adyen.checkout.core.components.internal
1010

1111
import androidx.annotation.RestrictTo
12+
import androidx.annotation.VisibleForTesting
1213
import com.adyen.checkout.core.components.CheckoutConfiguration
1314
import com.adyen.checkout.core.components.internal.ui.PaymentComponent
1415
import com.adyen.checkout.core.sessions.internal.model.SessionParams
1516
import kotlinx.coroutines.CoroutineScope
17+
import java.util.concurrent.ConcurrentHashMap
1618

1719
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
1820
object PaymentMethodProvider {
1921

20-
private val factories = mutableMapOf<String, PaymentMethodFactory<*, *>>()
22+
private val factories = ConcurrentHashMap<String, PaymentMethodFactory<*, *>>()
2123

2224
fun register(
2325
txVariant: String,
@@ -52,4 +54,20 @@ object PaymentMethodProvider {
5254
error("Factory for payment method type: $txVariant is not registered.")
5355
}
5456
}
57+
58+
/**
59+
* Clears all registered factories. Should only be used in tests.
60+
*/
61+
@VisibleForTesting
62+
internal fun clear() {
63+
factories.clear()
64+
}
65+
66+
/**
67+
* Returns the number of registered factories. Should only be used in tests.
68+
*/
69+
@VisibleForTesting
70+
internal fun getFactoriesCount(): Int {
71+
return factories.size
72+
}
5573
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright (c) 2025 Adyen N.V.
3+
*
4+
* This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
*
6+
* Created by ararat on 18/7/2025.
7+
*/
8+
9+
package com.adyen.checkout.core.components.internal
10+
11+
import com.adyen.checkout.core.common.Environment
12+
import com.adyen.checkout.core.components.CheckoutConfiguration
13+
import com.adyen.checkout.core.components.internal.ui.PaymentComponent
14+
import com.adyen.checkout.core.components.internal.ui.TestPaymentComponent
15+
import com.adyen.checkout.core.sessions.internal.model.SessionParams
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.test.runTest
18+
import org.junit.Assert
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Assert.assertNotSame
21+
import org.junit.Assert.assertTrue
22+
import org.junit.Before
23+
import org.junit.Test
24+
import org.junit.jupiter.api.assertThrows
25+
import java.util.Locale
26+
import java.util.concurrent.Executors
27+
import java.util.concurrent.TimeUnit
28+
29+
internal class PaymentMethodProviderTest {
30+
31+
private val component = TestPaymentComponent()
32+
private val factory = generateFactory(
33+
paymentComponent = component,
34+
)
35+
36+
@Before
37+
fun setUp() {
38+
PaymentMethodProvider.clear()
39+
}
40+
41+
@Test
42+
fun `when register is called concurrently from multiple threads, then provider should not lose any data`() {
43+
val threadCount = 10
44+
val registrationsPerThread = 100
45+
val totalRegistrations = threadCount * registrationsPerThread
46+
val executor = Executors.newFixedThreadPool(threadCount)
47+
48+
// Submit all registration tasks to the thread pool
49+
for (i in 0 until totalRegistrations) {
50+
executor.submit {
51+
PaymentMethodProvider.register("test_method_$i", factory)
52+
}
53+
}
54+
55+
// Wait for all tasks to complete
56+
executor.shutdown()
57+
// Giving it a generous timeout to finish all tasks
58+
val completed = executor.awaitTermination(5, TimeUnit.SECONDS)
59+
assertTrue("Executor tasks did not complete in time.", completed)
60+
assertEquals(totalRegistrations, PaymentMethodProvider.getFactoriesCount())
61+
}
62+
63+
@Test
64+
fun `when register is called with an existing txVariant, then the factory is overwritten`() =
65+
runTest {
66+
val secondaryComponent = TestPaymentComponent()
67+
val secondaryFactory = generateFactory(
68+
paymentComponent = secondaryComponent,
69+
)
70+
71+
PaymentMethodProvider.register("txVariant", factory)
72+
PaymentMethodProvider.register("txVariant", secondaryFactory)
73+
74+
val actualComponent = PaymentMethodProvider.get(
75+
txVariant = "txVariant",
76+
coroutineScope = this,
77+
checkoutConfiguration = generateCheckoutConfiguration(),
78+
componentSessionParams = null,
79+
)
80+
assertEquals(1, PaymentMethodProvider.getFactoriesCount())
81+
assertEquals(secondaryComponent, actualComponent)
82+
assertNotSame(component, actualComponent)
83+
}
84+
85+
@Test
86+
fun `when register is called for different txVariants, then all factories are stored`() {
87+
PaymentMethodProvider.register("txVariant_one", factory)
88+
PaymentMethodProvider.register("txVariant_two", factory)
89+
90+
assertEquals(2, PaymentMethodProvider.getFactoriesCount())
91+
}
92+
93+
@Test
94+
fun `when get is called for a registered factory, then the correct component is returned`() =
95+
runTest {
96+
PaymentMethodProvider.register("txVariant", factory)
97+
98+
val actualComponent = PaymentMethodProvider.get(
99+
txVariant = "txVariant",
100+
coroutineScope = this,
101+
checkoutConfiguration = generateCheckoutConfiguration(),
102+
componentSessionParams = null,
103+
)
104+
assertEquals(1, PaymentMethodProvider.getFactoriesCount())
105+
Assert.assertSame(component, actualComponent)
106+
}
107+
108+
@Test
109+
fun `when get is called for an unregistered factory, then an error is thrown`() = runTest {
110+
assertThrows<IllegalStateException> {
111+
PaymentMethodProvider.get(
112+
txVariant = "unregistered_txVariant",
113+
coroutineScope = this,
114+
checkoutConfiguration = generateCheckoutConfiguration(),
115+
componentSessionParams = null,
116+
)
117+
}
118+
}
119+
120+
@Test
121+
fun `when clear is called, then all factories are removed`() {
122+
PaymentMethodProvider.register("txVariant_one", factory)
123+
PaymentMethodProvider.register("txVariant_two", factory)
124+
assertEquals(2, PaymentMethodProvider.getFactoriesCount())
125+
126+
PaymentMethodProvider.clear()
127+
128+
assertEquals(0, PaymentMethodProvider.getFactoriesCount())
129+
}
130+
131+
private fun generateFactory(paymentComponent: PaymentComponent<BasePaymentComponentState>) =
132+
object :
133+
PaymentMethodFactory<BasePaymentComponentState, PaymentComponent<BasePaymentComponentState>> {
134+
override fun create(
135+
coroutineScope: CoroutineScope,
136+
checkoutConfiguration: CheckoutConfiguration,
137+
componentSessionParams: SessionParams?
138+
) = paymentComponent
139+
}
140+
141+
private fun generateCheckoutConfiguration() = CheckoutConfiguration(
142+
shopperLocale = Locale.US,
143+
environment = Environment.TEST,
144+
clientKey = "test_key_12345",
145+
)
146+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2025 Adyen N.V.
3+
*
4+
* This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
*
6+
* Created by ararat on 18/7/2025.
7+
*/
8+
9+
package com.adyen.checkout.core.components.internal.ui
10+
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Modifier
13+
import com.adyen.checkout.core.components.internal.BasePaymentComponentState
14+
import com.adyen.checkout.core.components.internal.PaymentComponentEvent
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.flowOf
17+
import java.util.UUID
18+
19+
internal data class TestPaymentComponent(
20+
private val id: String = UUID.randomUUID().toString(),
21+
) : PaymentComponent<BasePaymentComponentState> {
22+
override fun submit() {
23+
// No-op
24+
}
25+
26+
@Composable
27+
override fun ViewFactory(modifier: Modifier) {
28+
// No-op
29+
}
30+
31+
override val eventFlow: Flow<PaymentComponentEvent<BasePaymentComponentState>>
32+
get() = flowOf()
33+
}

0 commit comments

Comments
 (0)