diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-10-07 11:15:34 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-10-09 16:10:26 +0200 |
| commit | dcaccd122433c7b005193ea0a8e025ef2d4cf5ee (patch) | |
| tree | 00dc0307f298b2c06154c0cc1c902b0581751fa9 /android/lib/billing/src/test | |
| parent | 6c1f16c386c2d4cd3560c3a4cce7900f267058ad (diff) | |
| download | mullvadvpn-dcaccd122433c7b005193ea0a8e025ef2d4cf5ee.tar.xz mullvadvpn-dcaccd122433c7b005193ea0a8e025ef2d4cf5ee.zip | |
Migrate billing repository to unit test
Diffstat (limited to 'android/lib/billing/src/test')
| -rw-r--r-- | android/lib/billing/src/test/kotlin/android/content/Context.kt | 3 | ||||
| -rw-r--r-- | android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt | 403 |
2 files changed, 406 insertions, 0 deletions
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" + } +} |
