summaryrefslogtreecommitdiffhomepage
path: root/android/lib/billing
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-10-07 11:15:34 +0200
committerDavid Göransson <david.goransson@mullvad.net>2024-10-09 16:10:26 +0200
commitdcaccd122433c7b005193ea0a8e025ef2d4cf5ee (patch)
tree00dc0307f298b2c06154c0cc1c902b0581751fa9 /android/lib/billing
parent6c1f16c386c2d4cd3560c3a4cce7900f267058ad (diff)
downloadmullvadvpn-dcaccd122433c7b005193ea0a8e025ef2d4cf5ee.tar.xz
mullvadvpn-dcaccd122433c7b005193ea0a8e025ef2d4cf5ee.zip
Migrate billing repository to unit test
Diffstat (limited to 'android/lib/billing')
-rw-r--r--android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt383
-rw-r--r--android/lib/billing/src/test/kotlin/android/content/Context.kt3
-rw-r--r--android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt403
3 files changed, 406 insertions, 383 deletions
diff --git a/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt b/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt
deleted file mode 100644
index 083dfabff0..0000000000
--- a/android/lib/billing/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt
+++ /dev/null
@@ -1,383 +0,0 @@
-package net.mullvad.mullvadvpn.lib.billing
-
-import android.app.Activity
-import android.content.Context
-import app.cash.turbine.test
-import com.android.billingclient.api.BillingClient
-import com.android.billingclient.api.BillingClient.BillingResponseCode
-import com.android.billingclient.api.BillingClientStateListener
-import com.android.billingclient.api.BillingFlowParams
-import com.android.billingclient.api.BillingResult
-import com.android.billingclient.api.ProductDetails
-import com.android.billingclient.api.ProductDetailsResult
-import com.android.billingclient.api.Purchase
-import com.android.billingclient.api.PurchasesResult
-import com.android.billingclient.api.PurchasesUpdatedListener
-import com.android.billingclient.api.QueryPurchasesParams
-import com.android.billingclient.api.queryProductDetails
-import com.android.billingclient.api.queryPurchasesAsync
-import io.mockk.CapturingSlot
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.mockkStatic
-import io.mockk.unmockkAll
-import io.mockk.verify
-import kotlin.test.assertEquals
-import kotlin.test.assertIs
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.lib.billing.model.BillingException
-import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
-import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.lib.common.test.assertLists
-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
-
-@ExtendWith(TestCoroutineRule::class)
-class BillingRepositoryTest {
-
- private val mockContext: Context = mockk()
- private lateinit var billingRepository: BillingRepository
-
- private val mockBillingClientBuilder: BillingClient.Builder = mockk(relaxed = true)
- private val mockBillingClient: BillingClient = mockk()
-
- private val purchaseUpdatedListenerSlot: CapturingSlot<PurchasesUpdatedListener> =
- CapturingSlot()
-
- @BeforeEach
- fun setup() {
- mockkStatic(BILLING_CLIENT_CLASS)
- mockkStatic(BILLING_CLIENT_KOTLIN_CLASS)
- mockkStatic(BILLING_FLOW_PARAMS)
-
- every { BillingClient.newBuilder(any()) } returns mockBillingClientBuilder
- every { mockBillingClientBuilder.enablePendingPurchases(any()) } returns
- mockBillingClientBuilder
- every { mockBillingClientBuilder.setListener(capture(purchaseUpdatedListenerSlot)) } returns
- mockBillingClientBuilder
- every { mockBillingClientBuilder.build() } returns mockBillingClient
-
- billingRepository = BillingRepository(mockContext)
- }
-
- @AfterEach
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun testQueryProductsOk() = runTest {
- // Arrange
- val mockBillingResult: BillingResult = mockk()
- val mockProductDetails: ProductDetails = mockk()
- val expectedProductDetailsResult: ProductDetailsResult = mockk()
- val productId = "TEST"
- val price = "44.4"
-
- every { mockBillingResult.responseCode } returns BillingResponseCode.OK
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryProductDetails(any()) } returns
- expectedProductDetailsResult
- every { expectedProductDetailsResult.billingResult } returns mockBillingResult
- every { expectedProductDetailsResult.productDetailsList } returns listOf(mockProductDetails)
- every { mockProductDetails.productId } returns productId
- every { mockProductDetails.oneTimePurchaseOfferDetails?.formattedPrice } returns price
-
- // Act
- val result = billingRepository.queryProducts(listOf(productId))
-
- // Assert
- assertEquals(expectedProductDetailsResult, result)
- }
-
- @Test
- fun testQueryProductsItemUnavailable() = runTest {
- // Arrange
- val mockBillingResult: BillingResult = mockk()
- val mockProductDetailsResult: ProductDetailsResult = mockk()
-
- every { mockBillingResult.responseCode } returns BillingResponseCode.ITEM_UNAVAILABLE
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryProductDetails(any()) } returns mockProductDetailsResult
- every { mockProductDetailsResult.billingResult } returns mockBillingResult
- every { mockProductDetailsResult.productDetailsList } returns emptyList()
-
- // Act
- val result = billingRepository.queryProducts(listOf("TEST"))
-
- // Assert
- assertEquals(mockProductDetailsResult, result)
- }
-
- @Test
- fun testQueryProductsBillingUnavailable() = runTest {
- // Arrange
- val mockBillingResult: BillingResult = mockk()
- val mockProductDetailsResult: ProductDetailsResult = mockk()
-
- every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryProductDetails(any()) } returns mockProductDetailsResult
- every { mockProductDetailsResult.billingResult } returns mockBillingResult
- every { mockProductDetailsResult.productDetailsList } returns emptyList()
-
- // Act
- val result = billingRepository.queryProducts(listOf("TEST"))
-
- // Assert
- assertEquals(mockProductDetailsResult, result)
- }
-
- @Test
- fun testStartPurchaseFlowOk() = runTest {
- // Arrange
- val mockProductBillingResult: BillingResult = mockk()
- val mockBillingResult: BillingResult = mockk()
- val transactionId = "MOCK22"
- val mockProductDetails: ProductDetails = mockk(relaxed = true)
- val mockActivityProvider: () -> Activity = mockk()
- every { mockBillingResult.responseCode } returns BillingResponseCode.OK
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- every { mockBillingClient.launchBillingFlow(any(), any()) } returns mockBillingResult
- every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
- every { mockProductBillingResult.responseCode } returns BillingResponseCode.OK
- every { mockActivityProvider() } returns mockk()
-
- // Act
- val result =
- billingRepository.startPurchaseFlow(
- mockProductDetails,
- transactionId,
- mockActivityProvider,
- )
-
- // Assert
- assertEquals(mockBillingResult, result)
- }
-
- @Test
- fun testStartPurchaseFlowBillingUnavailable() = runTest {
- // Arrange
- val mockBillingResult: BillingResult = mockk()
- val transactionId = "MOCK22"
- val mockProductDetails: ProductDetails = mockk(relaxed = true)
- val mockActivityProvider: () -> Activity = mockk()
- every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- every { mockBillingClient.launchBillingFlow(any(), any()) } returns mockBillingResult
- every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
- every { mockActivityProvider() } returns mockk()
-
- // Act
- val result =
- billingRepository.startPurchaseFlow(
- mockProductDetails,
- transactionId,
- mockActivityProvider,
- )
-
- // Assert
- assertEquals(mockBillingResult, result)
- }
-
- @Test
- fun testQueryPurchasesFoundPurchases() = runTest {
- // Arrange
- val mockResult: PurchasesResult = mockk()
- val mockPurchase: Purchase = mockk()
- every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK
- every { mockResult.purchasesList } returns listOf(mockPurchase)
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
- mockResult
- every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
-
- // Act
- val result = billingRepository.queryPurchases()
-
- // Assert
- assertEquals(mockResult, result)
- }
-
- @Test
- fun testQueryPurchasesNoPurchaseFound() = runTest {
- // Arrange
- val mockResult: PurchasesResult = mockk()
- every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK
- every { mockResult.purchasesList } returns emptyList()
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
- mockResult
- every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
-
- // Act
- val result = billingRepository.queryPurchases()
-
- // Assert
- assertEquals(mockResult, result)
- }
-
- @Test
- fun testQueryPurchasesError() = runTest {
- // Arrange
- val responseCode = BillingResponseCode.ITEM_UNAVAILABLE
- val message = "ERROR"
- val expectedError = BillingException(responseCode, message)
- val mockResult: PurchasesResult = mockk()
- every { mockResult.billingResult.responseCode } returns responseCode
- every { mockResult.billingResult.debugMessage } returns message
- every { mockResult.purchasesList } returns emptyList()
- every { mockBillingClient.isReady } returns true
- every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED
- coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
- mockResult
- every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
-
- // Act
- val result = billingRepository.queryPurchases()
-
- // Assert
- assertEquals(
- expectedError.toBillingResult().responseCode,
- result.billingResult.responseCode,
- )
- assertEquals(expectedError.message, result.billingResult.debugMessage)
- }
-
- @Test
- fun testPurchaseEventPurchaseComplete() = runTest {
- // Arrange
- val mockPurchase: Purchase = mockk()
- val mockPurchaseList = listOf(mockPurchase)
- val mockBillingResult: BillingResult = mockk()
- every { mockBillingResult.responseCode } returns BillingResponseCode.OK
-
- // Act, Assert
- billingRepository.purchaseEvents.test {
- purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(
- mockBillingResult,
- mockPurchaseList,
- )
- val result = awaitItem()
- assertIs<PurchaseEvent.Completed>(result)
- assertLists(mockPurchaseList, result.purchases)
- }
- }
-
- @Test
- fun testPurchaseEventUserCanceled() = runTest {
- // Arrange
- val mockBillingResult: BillingResult = mockk()
- val mockResponseCode: Int = BillingResponseCode.USER_CANCELED
- every { mockBillingResult.responseCode } returns mockResponseCode
-
- // Act, Assert
- billingRepository.purchaseEvents.test {
- purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(mockBillingResult, null)
- val result = awaitItem()
- assertIs<PurchaseEvent.UserCanceled>(result)
- }
- }
-
- @Test
- fun testPurchaseEventError() = runTest {
- // Arrange
- val mockDebugMessage = "ERROR"
- val mockBillingResult: BillingResult = mockk()
- val mockResponseCode: Int = BillingResponseCode.ERROR
- val expectedError =
- BillingException(responseCode = mockResponseCode, message = mockDebugMessage)
- every { mockBillingResult.responseCode } returns mockResponseCode
- every { mockBillingResult.debugMessage } returns mockDebugMessage
-
- // Act, Assert
- billingRepository.purchaseEvents.test {
- purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(mockBillingResult, null)
- val result = awaitItem()
- assertIs<PurchaseEvent.Error>(result)
- assertEquals(expectedError.message, result.exception.message)
- }
- }
-
- @Test
- fun testEnsureConnectedStartConnection() = runTest {
- // Arrange
- val mockStartConnectionResult: BillingResult = mockk()
- every { mockBillingClient.isReady } returns false
- every { mockBillingClient.connectionState } returns
- BillingClient.ConnectionState.DISCONNECTED
- every { mockBillingClient.startConnection(any()) } answers
- {
- firstArg<BillingClientStateListener>()
- .onBillingSetupFinished(mockStartConnectionResult)
- }
- every { mockStartConnectionResult.responseCode } returns BillingResponseCode.OK
- coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
- mockk(relaxed = true)
-
- // Act
- billingRepository.queryPurchases()
-
- // Assert
- verify { mockBillingClient.startConnection(any()) }
- coVerify { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun testEnsureConnectedOnlyOneSuccessfulConnection() =
- runTest(UnconfinedTestDispatcher()) {
- // Arrange
- var hasConnected = false
- val mockStartConnectionResult: BillingResult = mockk()
- every { mockBillingClient.isReady } answers { hasConnected }
- every { mockBillingClient.connectionState } answers
- {
- if (hasConnected) {
- BillingClient.ConnectionState.CONNECTED
- } else {
- BillingClient.ConnectionState.DISCONNECTED
- }
- }
- every { mockBillingClient.startConnection(any()) } answers
- {
- hasConnected = true
- firstArg<BillingClientStateListener>()
- .onBillingSetupFinished(mockStartConnectionResult)
- }
- every { mockStartConnectionResult.responseCode } returns BillingResponseCode.OK
- coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
- mockk(relaxed = true)
- coEvery { mockBillingClient.queryProductDetails(any()) } returns mockk(relaxed = true)
-
- // Act
- launch { billingRepository.queryPurchases() }
- launch { billingRepository.queryProducts(listOf("MOCK")) }
-
- // Assert
- verify(exactly = 1) { mockBillingClient.startConnection(any()) }
- coVerify { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) }
- coVerify { mockBillingClient.queryProductDetails(any()) }
- }
-
- companion object {
- private const val BILLING_CLIENT_CLASS = "com.android.billingclient.api.BillingClient"
- private const val BILLING_CLIENT_KOTLIN_CLASS =
- "com.android.billingclient.api.BillingClientKotlinKt"
- private const val BILLING_FLOW_PARAMS = "com.android.billingclient.api.BillingFlowParams"
- }
-}
diff --git a/android/lib/billing/src/test/kotlin/android/content/Context.kt b/android/lib/billing/src/test/kotlin/android/content/Context.kt
new file mode 100644
index 0000000000..26297fadff
--- /dev/null
+++ b/android/lib/billing/src/test/kotlin/android/content/Context.kt
@@ -0,0 +1,3 @@
+package android.content
+
+open class Context
diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt
new file mode 100644
index 0000000000..3313e2d91e
--- /dev/null
+++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt
@@ -0,0 +1,403 @@
+package net.mullvad.mullvadvpn.lib.billing
+
+import android.app.Activity
+import android.content.Context
+import app.cash.turbine.test
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClient.BillingResponseCode
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.ProductDetailsResult
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.PurchasesResult
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryPurchasesParams
+import com.android.billingclient.api.queryProductDetails
+import com.android.billingclient.api.queryPurchasesAsync
+import io.mockk.CapturingSlot
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.mockk.verify
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.billing.model.BillingException
+import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
+import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
+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
+
+@ExtendWith(TestCoroutineRule::class)
+class BillingRepositoryTest {
+
+ private val mockContext: Context = mockk()
+ private lateinit var billingRepository: BillingRepository
+
+ private val mockBillingClientBuilder: BillingClient.Builder = mockk(relaxed = true)
+ private val mockBillingClient: BillingClient = mockk()
+
+ private val purchaseUpdatedListenerSlot: CapturingSlot<PurchasesUpdatedListener> =
+ CapturingSlot()
+
+ @BeforeEach
+ fun setup() {
+ mockkStatic(BILLING_CLIENT_CLASS)
+ mockkStatic(BILLING_CLIENT_KOTLIN_CLASS)
+ mockkStatic(BILLING_FLOW_PARAMS)
+
+ every { BillingClient.newBuilder(any()) } returns mockBillingClientBuilder
+ every { mockBillingClientBuilder.enablePendingPurchases(any()) } returns
+ mockBillingClientBuilder
+ every { mockBillingClientBuilder.setListener(capture(purchaseUpdatedListenerSlot)) } returns
+ mockBillingClientBuilder
+ every { mockBillingClientBuilder.build() } returns mockBillingClient
+
+ billingRepository = BillingRepository(mockContext)
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `when billing client query product details returns OK query products should return OK`() =
+ runTest {
+ // Arrange
+ val mockBillingResult: BillingResult = mockk()
+ val mockProductDetails: ProductDetails = mockk()
+ val expectedProductDetailsResult: ProductDetailsResult = mockk()
+ val productId = "TEST"
+ val price = "44.4"
+
+ every { mockBillingResult.responseCode } returns BillingResponseCode.OK
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryProductDetails(any()) } returns
+ expectedProductDetailsResult
+ every { expectedProductDetailsResult.billingResult } returns mockBillingResult
+ every { expectedProductDetailsResult.productDetailsList } returns
+ listOf(mockProductDetails)
+ every { mockProductDetails.productId } returns productId
+ every { mockProductDetails.oneTimePurchaseOfferDetails?.formattedPrice } returns price
+
+ // Act
+ val result = billingRepository.queryProducts(listOf(productId))
+
+ // Assert
+ assertEquals(expectedProductDetailsResult, result)
+ }
+
+ @Test
+ fun `when billing client query product details returns empty list query products should return empty list`() =
+ runTest {
+ // Arrange
+ val mockBillingResult: BillingResult = mockk()
+ val mockProductDetailsResult: ProductDetailsResult = mockk()
+
+ every { mockBillingResult.responseCode } returns BillingResponseCode.ITEM_UNAVAILABLE
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryProductDetails(any()) } returns
+ mockProductDetailsResult
+ every { mockProductDetailsResult.billingResult } returns mockBillingResult
+ every { mockProductDetailsResult.productDetailsList } returns emptyList()
+
+ // Act
+ val result = billingRepository.queryProducts(listOf("TEST"))
+
+ // Assert
+ assertEquals(mockProductDetailsResult, result)
+ }
+
+ @Test
+ fun `when billing client query product details returns billing unavailable query products should return billing unavailable`() =
+ runTest {
+ // Arrange
+ val mockBillingResult: BillingResult = mockk()
+ val mockProductDetailsResult: ProductDetailsResult = mockk()
+
+ every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryProductDetails(any()) } returns
+ mockProductDetailsResult
+ every { mockProductDetailsResult.billingResult } returns mockBillingResult
+ every { mockProductDetailsResult.productDetailsList } returns emptyList()
+
+ // Act
+ val result = billingRepository.queryProducts(listOf("TEST"))
+
+ // Assert
+ assertEquals(mockProductDetailsResult, result)
+ }
+
+ @Test
+ fun `when billing client launch billing flow returns OK start purchase flow should return OK`() =
+ runTest {
+ // Arrange
+ val mockProductBillingResult: BillingResult = mockk()
+ val mockBillingResult: BillingResult = mockk()
+ val transactionId = "MOCK22"
+ val mockProductDetails: ProductDetails = mockk(relaxed = true)
+ val mockActivityProvider: () -> Activity = mockk()
+ every { mockBillingResult.responseCode } returns BillingResponseCode.OK
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ every { mockBillingClient.launchBillingFlow(any(), any()) } returns mockBillingResult
+ every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
+ every { mockProductBillingResult.responseCode } returns BillingResponseCode.OK
+ every { mockActivityProvider() } returns mockk()
+
+ // Act
+ val result =
+ billingRepository.startPurchaseFlow(
+ mockProductDetails,
+ transactionId,
+ mockActivityProvider,
+ )
+
+ // Assert
+ assertEquals(mockBillingResult, result)
+ }
+
+ @Test
+ fun `when billing client launch billing flow returns unavailable start purchase flow should return unavailable`() =
+ runTest {
+ // Arrange
+ val mockBillingResult: BillingResult = mockk()
+ val transactionId = "MOCK22"
+ val mockProductDetails: ProductDetails = mockk(relaxed = true)
+ val mockActivityProvider: () -> Activity = mockk()
+ every { mockBillingResult.responseCode } returns BillingResponseCode.BILLING_UNAVAILABLE
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ every { mockBillingClient.launchBillingFlow(any(), any()) } returns mockBillingResult
+ every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
+ every { mockActivityProvider() } returns mockk()
+
+ // Act
+ val result =
+ billingRepository.startPurchaseFlow(
+ mockProductDetails,
+ transactionId,
+ mockActivityProvider,
+ )
+
+ // Assert
+ assertEquals(mockBillingResult, result)
+ }
+
+ @Test
+ fun `when billing client query purchases returns OK query purchases should return OK`() =
+ runTest {
+ // Arrange
+ val mockResult: PurchasesResult = mockk()
+ val mockPurchase: Purchase = mockk()
+ every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK
+ every { mockResult.purchasesList } returns listOf(mockPurchase)
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
+ mockResult
+ every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
+
+ // Act
+ val result = billingRepository.queryPurchases()
+
+ // Assert
+ assertEquals(mockResult, result)
+ }
+
+ @Test
+ fun `when billing client query purchases returns empty list query purchases should return empty list`() =
+ runTest {
+ // Arrange
+ val mockResult: PurchasesResult = mockk()
+ every { mockResult.billingResult.responseCode } returns BillingResponseCode.OK
+ every { mockResult.purchasesList } returns emptyList()
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
+ mockResult
+ every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
+
+ // Act
+ val result = billingRepository.queryPurchases()
+
+ // Assert
+ assertEquals(mockResult, result)
+ }
+
+ @Test
+ fun `when billing client query purchases returns unavailable query purchases should return unavailable`() =
+ runTest {
+ // Arrange
+ val responseCode = BillingResponseCode.ITEM_UNAVAILABLE
+ val message = "ERROR"
+ val expectedError = BillingException(responseCode, message)
+ val mockResult: PurchasesResult = mockk()
+ every { mockResult.billingResult.responseCode } returns responseCode
+ every { mockResult.billingResult.debugMessage } returns message
+ every { mockResult.purchasesList } returns emptyList()
+ every { mockBillingClient.isReady } returns true
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.CONNECTED
+ coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
+ mockResult
+ every { BillingFlowParams.newBuilder() } returns mockk(relaxed = true)
+
+ // Act
+ val result = billingRepository.queryPurchases()
+
+ // Assert
+ assertEquals(
+ expectedError.toBillingResult().responseCode,
+ result.billingResult.responseCode,
+ )
+ assertEquals(expectedError.message, result.billingResult.debugMessage)
+ }
+
+ @Test
+ fun `when onPurchasesUpdated returns OK purchase event should return completed`() = runTest {
+ // Arrange
+ val mockPurchase: Purchase = mockk()
+ val mockPurchaseList = listOf(mockPurchase)
+ val mockBillingResult: BillingResult = mockk()
+ every { mockBillingResult.responseCode } returns BillingResponseCode.OK
+
+ // Act, Assert
+ billingRepository.purchaseEvents.test {
+ purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(
+ mockBillingResult,
+ mockPurchaseList,
+ )
+ val result = awaitItem()
+ assertIs<PurchaseEvent.Completed>(result)
+ assertLists(mockPurchaseList, result.purchases)
+ }
+ }
+
+ @Test
+ fun `when onPurchasesUpdated returns user canceled purchase event should return user canceled`() =
+ runTest {
+ // Arrange
+ val mockBillingResult: BillingResult = mockk()
+ val mockResponseCode: Int = BillingResponseCode.USER_CANCELED
+ every { mockBillingResult.responseCode } returns mockResponseCode
+
+ // Act, Assert
+ billingRepository.purchaseEvents.test {
+ purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(mockBillingResult, null)
+ val result = awaitItem()
+ assertIs<PurchaseEvent.UserCanceled>(result)
+ }
+ }
+
+ @Test
+ fun `when onPurchasesUpdated returns error purchase event should return error`() = runTest {
+ // Arrange
+ val mockDebugMessage = "ERROR"
+ val mockBillingResult: BillingResult = mockk()
+ val mockResponseCode: Int = BillingResponseCode.ERROR
+ val expectedError =
+ BillingException(responseCode = mockResponseCode, message = mockDebugMessage)
+ every { mockBillingResult.responseCode } returns mockResponseCode
+ every { mockBillingResult.debugMessage } returns mockDebugMessage
+
+ // Act, Assert
+ billingRepository.purchaseEvents.test {
+ purchaseUpdatedListenerSlot.captured.onPurchasesUpdated(mockBillingResult, null)
+ val result = awaitItem()
+ assertIs<PurchaseEvent.Error>(result)
+ assertEquals(expectedError.message, result.exception.message)
+ }
+ }
+
+ @Test
+ fun `when billing client is not ready start connection should be called`() = runTest {
+ // Arrange
+ val mockStartConnectionResult: BillingResult = mockk()
+ every { mockBillingClient.isReady } returns false
+ every { mockBillingClient.connectionState } returns
+ BillingClient.ConnectionState.DISCONNECTED
+ every { mockBillingClient.startConnection(any()) } answers
+ {
+ firstArg<BillingClientStateListener>()
+ .onBillingSetupFinished(mockStartConnectionResult)
+ }
+ every { mockStartConnectionResult.responseCode } returns BillingResponseCode.OK
+ coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
+ mockk(relaxed = true)
+
+ // Act
+ billingRepository.queryPurchases()
+
+ // Assert
+ verify { mockBillingClient.startConnection(any()) }
+ coVerify { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `ensure only one billing client start connection is called`() =
+ runTest(UnconfinedTestDispatcher()) {
+ // Arrange
+ var hasConnected = false
+ val mockStartConnectionResult: BillingResult = mockk()
+ every { mockBillingClient.isReady } answers { hasConnected }
+ every { mockBillingClient.connectionState } answers
+ {
+ if (hasConnected) {
+ BillingClient.ConnectionState.CONNECTED
+ } else {
+ BillingClient.ConnectionState.DISCONNECTED
+ }
+ }
+ every { mockBillingClient.startConnection(any()) } answers
+ {
+ hasConnected = true
+ firstArg<BillingClientStateListener>()
+ .onBillingSetupFinished(mockStartConnectionResult)
+ }
+ every { mockStartConnectionResult.responseCode } returns BillingResponseCode.OK
+ coEvery { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) } returns
+ mockk(relaxed = true)
+ coEvery { mockBillingClient.queryProductDetails(any()) } returns mockk(relaxed = true)
+
+ // Act
+ launch { billingRepository.queryPurchases() }
+ launch { billingRepository.queryProducts(listOf("MOCK")) }
+
+ // Assert
+ verify(exactly = 1) { mockBillingClient.startConnection(any()) }
+ coVerify { mockBillingClient.queryPurchasesAsync(any<QueryPurchasesParams>()) }
+ coVerify { mockBillingClient.queryProductDetails(any()) }
+ }
+
+ companion object {
+ private const val BILLING_CLIENT_CLASS = "com.android.billingclient.api.BillingClient"
+ private const val BILLING_CLIENT_KOTLIN_CLASS =
+ "com.android.billingclient.api.BillingClientKotlinKt"
+ private const val BILLING_FLOW_PARAMS = "com.android.billingclient.api.BillingFlowParams"
+ }
+}