summaryrefslogtreecommitdiffhomepage
path: root/android/app/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'android/app/src/test')
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt104
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt141
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt152
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt132
4 files changed, 521 insertions, 8 deletions
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt
new file mode 100644
index 0000000000..a1d8bee37a
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/PlayPaymentUseCaseTest.kt
@@ -0,0 +1,104 @@
+package net.mullvad.mullvadvpn.usecase
+
+import app.cash.turbine.test
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.payment.PaymentRepository
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
+import org.junit.Test
+
+class PlayPaymentUseCaseTest {
+
+ private val mockPaymentRepository: PaymentRepository = mockk(relaxed = true)
+
+ private val playPaymentUseCase = PlayPaymentUseCase(mockPaymentRepository)
+
+ @Test
+ fun testUpdatePaymentAvailability() = runTest {
+ // Arrange
+ val productsUnavailable = PaymentAvailability.ProductsUnavailable
+ val paymentRepositoryQueryPaymentAvailabilityFlow = flow { emit(productsUnavailable) }
+ every { mockPaymentRepository.queryPaymentAvailability() } returns
+ paymentRepositoryQueryPaymentAvailabilityFlow
+
+ // Act, Assert
+ playPaymentUseCase.paymentAvailability.test {
+ assertNull(awaitItem())
+ playPaymentUseCase.queryPaymentAvailability()
+ assertEquals(productsUnavailable, awaitItem())
+ }
+ }
+
+ @Test
+ fun testUpdatePurchaseResult() = runTest {
+ // Arrange
+ val fetchingProducts = PurchaseResult.FetchingProducts
+ val productId = ProductId("productId")
+ val paymentRepositoryPurchaseResultFlow = flow { emit(fetchingProducts) }
+ every { mockPaymentRepository.purchaseProduct(any(), any()) } returns
+ paymentRepositoryPurchaseResultFlow
+
+ // Act, Assert
+ playPaymentUseCase.purchaseResult.test {
+ assertNull(awaitItem())
+ playPaymentUseCase.purchaseProduct(productId, mockk())
+ assertEquals(fetchingProducts, awaitItem())
+ }
+ }
+
+ @Test
+ fun testPurchaseProduct() = runTest {
+ // Arrange
+ val productId = ProductId("productId")
+
+ // Act
+ playPaymentUseCase.purchaseProduct(productId, mockk())
+
+ // Assert
+ coVerify { mockPaymentRepository.purchaseProduct(productId, any()) }
+ }
+
+ @Test
+ fun testQueryPaymentAvailability() = runTest {
+ // Act
+ playPaymentUseCase.queryPaymentAvailability()
+
+ // Assert
+ coVerify { mockPaymentRepository.queryPaymentAvailability() }
+ }
+
+ @Test
+ fun testResetPurchaseResult() = runTest {
+ // Arrange
+ val completedSuccess = PurchaseResult.Completed.Success
+ val productId = ProductId("productId")
+ val paymentRepositoryPurchaseResultFlow = flow { emit(completedSuccess) }
+ every { mockPaymentRepository.purchaseProduct(any(), any()) } returns
+ paymentRepositoryPurchaseResultFlow
+
+ // Act, Assert
+ playPaymentUseCase.purchaseResult.test {
+ assertNull(awaitItem())
+ playPaymentUseCase.purchaseProduct(productId, mockk())
+ assertEquals(completedSuccess, awaitItem())
+ playPaymentUseCase.resetPurchaseResult()
+ assertNull(awaitItem())
+ }
+ }
+
+ @Test
+ fun testVerifyPurchases() = runTest {
+ // Act
+ playPaymentUseCase.verifyPurchases()
+
+ // Assert
+ coVerify { mockPaymentRepository.verifyPurchases() }
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
index fc1fd5e99b..c02e755951 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
@@ -1,15 +1,27 @@
package net.mullvad.mullvadvpn.viewmodel
+import android.app.Activity
import app.cash.turbine.test
+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 kotlin.test.assertNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
+import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.model.AccountAndDevice
import net.mullvad.mullvadvpn.model.AccountExpiry
import net.mullvad.mullvadvpn.model.Device
@@ -19,6 +31,8 @@ import net.mullvad.mullvadvpn.repository.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import net.mullvad.mullvadvpn.util.toPaymentDialogData
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -31,8 +45,11 @@ class AccountViewModelTest {
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockDeviceRepository: DeviceRepository = mockk()
private val mockAuthTokenCache: AuthTokenCache = mockk()
+ private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
private val deviceState: MutableStateFlow<DeviceState> = MutableStateFlow(DeviceState.Initial)
+ private val paymentAvailability = MutableStateFlow<PaymentAvailability?>(null)
+ private val purchaseResult = MutableStateFlow<PurchaseResult?>(null)
private val accountExpiryState = MutableStateFlow(AccountExpiry.Missing)
private val dummyAccountAndDevice: AccountAndDevice =
@@ -51,15 +68,19 @@ class AccountViewModelTest {
@Before
fun setUp() {
mockkStatic(CACHE_EXTENSION_CLASS)
+ mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache
every { mockDeviceRepository.deviceState } returns deviceState
every { mockAccountRepository.accountExpiryState } returns accountExpiryState
+ coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResult
+ coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
viewModel =
AccountViewModel(
accountRepository = mockAccountRepository,
serviceConnectionManager = mockServiceConnectionManager,
- deviceRepository = mockDeviceRepository
+ deviceRepository = mockDeviceRepository,
+ paymentUseCase = mockPaymentUseCase
)
}
@@ -72,10 +93,9 @@ class AccountViewModelTest {
fun testAccountLoggedInState() = runTest {
// Act, Assert
viewModel.uiState.test {
- var result = awaitItem()
- assertEquals(null, result.deviceName)
+ awaitItem() // Default state
deviceState.value = DeviceState.LoggedIn(accountAndDevice = dummyAccountAndDevice)
- result = awaitItem()
+ val result = awaitItem()
assertEquals(DUMMY_DEVICE_NAME, result.accountNumber)
}
}
@@ -89,8 +109,121 @@ class AccountViewModelTest {
verify { mockAccountRepository.logout() }
}
+ @Test
+ fun testBillingProductsUnavailableState() = runTest {
+ // Arrange in setup
+
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Default state
+ paymentAvailability.tryEmit(PaymentAvailability.ProductsUnavailable)
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.NoPayment>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsGenericErrorState() = runTest {
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Default state
+ paymentAvailability.tryEmit(PaymentAvailability.Error.Other(mockk()))
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Generic>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsBillingErrorState() = runTest {
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Default state
+ paymentAvailability.tryEmit(PaymentAvailability.Error.BillingUnavailable)
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Billing>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsPaymentAvailableState() = runTest {
+ // Arrange
+ val mockProduct: PaymentProduct = mockk()
+ val expectedProductList = listOf(mockProduct)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ awaitItem() // Default state
+ paymentAvailability.tryEmit(PaymentAvailability.ProductsAvailable(listOf(mockProduct)))
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.PaymentAvailable>(result)
+ assertLists(expectedProductList, result.products)
+ }
+ }
+
+ @Test
+ fun testBillingUserCancelled() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Cancelled
+ purchaseResult.value = result
+ every { result.toPaymentDialogData() } returns null
+
+ // Act, Assert
+ viewModel.uiState.test { assertNull(awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testBillingPurchaseSuccess() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Success
+ val expectedData: PaymentDialogData = mockk()
+ purchaseResult.value = result
+ every { result.toPaymentDialogData() } returns expectedData
+
+ // Act, Assert
+ viewModel.uiState.test { assertEquals(expectedData, awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testStartBillingPayment() {
+ // Arrange
+ val mockProductId = ProductId("MOCK")
+ val mockActivityProvider = mockk<() -> Activity>()
+
+ // Act
+ viewModel.startBillingPayment(mockProductId, mockActivityProvider)
+
+ // Assert
+ coVerify { mockPaymentUseCase.purchaseProduct(mockProductId, mockActivityProvider) }
+ }
+
+ @Test
+ fun testOnClosePurchaseResultDialogSuccessful() {
+ // Arrange
+
+ // Act
+ viewModel.onClosePurchaseResultDialog(success = true)
+
+ // Assert
+ verify { mockAccountRepository.fetchAccountExpiry() }
+ coVerify { mockPaymentUseCase.resetPurchaseResult() }
+ }
+
+ @Test
+ fun testOnClosePurchaseResultDialogNotSuccessful() {
+ // Arrange
+
+ // Act
+ viewModel.onClosePurchaseResultDialog(success = false)
+
+ // Assert
+ coVerify { mockPaymentUseCase.queryPaymentAvailability() }
+ coVerify { mockPaymentUseCase.resetPurchaseResult() }
+ }
+
companion object {
private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
+ private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
+ "net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
private const val DUMMY_DEVICE_NAME = "fake_name"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
index 8c1ec10f5a..dad51eab59 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
@@ -1,8 +1,10 @@
package net.mullvad.mullvadvpn.viewmodel
+import android.app.Activity
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
@@ -10,11 +12,19 @@ import io.mockk.unmockkAll
import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
+import kotlin.test.assertNull
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
+import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.model.AccountExpiry
import net.mullvad.mullvadvpn.model.DeviceState
import net.mullvad.mullvadvpn.model.TunnelState
@@ -27,6 +37,8 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import net.mullvad.mullvadvpn.util.toPaymentDialogData
import net.mullvad.talpid.util.EventNotifier
import org.joda.time.DateTime
import org.joda.time.ReadableInstant
@@ -42,6 +54,8 @@ class OutOfTimeViewModelTest {
MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial)
+ private val paymentAvailability = MutableStateFlow<PaymentAvailability?>(null)
+ private val purchaseResult = MutableStateFlow<PurchaseResult?>(null)
// Service connections
private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
@@ -50,15 +64,17 @@ class OutOfTimeViewModelTest {
// Event notifiers
private val eventNotifierTunnelRealState = EventNotifier<TunnelState>(TunnelState.Disconnected)
- private val mockAccountRepository: AccountRepository = mockk()
+ private val mockAccountRepository: AccountRepository = mockk(relaxed = true)
private val mockDeviceRepository: DeviceRepository = mockk()
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
+ private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
private lateinit var viewModel: OutOfTimeViewModel
@Before
fun setUp() {
mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
+ mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
@@ -70,11 +86,16 @@ class OutOfTimeViewModelTest {
every { mockDeviceRepository.deviceState } returns deviceState
+ coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResult
+
+ coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
+
viewModel =
OutOfTimeViewModel(
accountRepository = mockAccountRepository,
serviceConnectionManager = mockServiceConnectionManager,
deviceRepository = mockDeviceRepository,
+ paymentUseCase = mockPaymentUseCase,
pollAccountExpiry = false
)
}
@@ -112,9 +133,9 @@ class OutOfTimeViewModelTest {
// Act, Assert
viewModel.uiState.test {
assertEquals(OutOfTimeUiState(deviceName = ""), awaitItem())
+ eventNotifierTunnelRealState.notify(tunnelRealStateTestItem)
serviceConnectionState.value =
ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelRealState.notify(tunnelRealStateTestItem)
val result = awaitItem()
assertEquals(tunnelRealStateTestItem, result.tunnelState)
}
@@ -149,8 +170,135 @@ class OutOfTimeViewModelTest {
verify { mockProxy.disconnect() }
}
+ @Test
+ fun testBillingProductsUnavailableState() = runTest {
+ // Arrange
+ val productsUnavailable = PaymentAvailability.ProductsUnavailable
+ paymentAvailability.value = productsUnavailable
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.NoPayment>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsGenericErrorState() = runTest {
+ // Arrange
+ val paymentAvailabilityError = PaymentAvailability.Error.Other(mockk())
+ paymentAvailability.value = paymentAvailabilityError
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Generic>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsBillingErrorState() = runTest {
+ // Arrange
+ val paymentAvailabilityError = PaymentAvailability.Error.BillingUnavailable
+ paymentAvailability.value = paymentAvailabilityError
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Billing>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsPaymentAvailableState() = runTest {
+ // Arrange
+ val mockProduct: PaymentProduct = mockk()
+ val expectedProductList = listOf(mockProduct)
+ val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct))
+ paymentAvailability.value = productsAvailable
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.PaymentAvailable>(result)
+ assertLists(expectedProductList, result.products)
+ }
+ }
+
+ @Test
+ fun testBillingUserCancelled() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Cancelled
+ purchaseResult.value = result
+ every { result.toPaymentDialogData() } returns null
+
+ // Act, Assert
+ viewModel.uiState.test { assertNull(awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testBillingPurchaseSuccess() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Success
+ val expectedData: PaymentDialogData = mockk()
+ purchaseResult.value = result
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ every { result.toPaymentDialogData() } returns expectedData
+
+ // Act, Assert
+ viewModel.uiState.test { assertEquals(expectedData, awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testStartBillingPayment() {
+ // Arrange
+ val mockProductId = ProductId("MOCK")
+ val mockActivityProvider = mockk<() -> Activity>()
+
+ // Act
+ viewModel.startBillingPayment(mockProductId, mockActivityProvider)
+
+ // Assert
+ coVerify { mockPaymentUseCase.purchaseProduct(mockProductId, mockActivityProvider) }
+ }
+
+ @Test
+ fun testOnClosePurchaseResultDialogSuccessful() {
+ // Arrange
+
+ // Act
+ viewModel.onClosePurchaseResultDialog(success = true)
+
+ // Assert
+ verify { mockAccountRepository.fetchAccountExpiry() }
+ coVerify { mockPaymentUseCase.resetPurchaseResult() }
+ }
+
+ @Test
+ fun testOnClosePurchaseResultDialogNotSuccessful() {
+ // Arrange
+
+ // Act
+ viewModel.onClosePurchaseResultDialog(success = false)
+
+ // Assert
+ coVerify { mockPaymentUseCase.queryPaymentAvailability() }
+ coVerify { mockPaymentUseCase.resetPurchaseResult() }
+ }
+
companion object {
private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
"net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
+ private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
+ "net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
index b16eeec2f8..e958df9337 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
@@ -1,19 +1,29 @@
package net.mullvad.mullvadvpn.viewmodel
+import android.app.Activity
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
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 kotlin.test.assertEquals
import kotlin.test.assertIs
+import kotlin.test.assertNull
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
+import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.model.AccountAndDevice
import net.mullvad.mullvadvpn.model.AccountExpiry
import net.mullvad.mullvadvpn.model.Device
@@ -27,6 +37,8 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import net.mullvad.mullvadvpn.util.toPaymentDialogData
import net.mullvad.talpid.util.EventNotifier
import org.joda.time.DateTime
import org.joda.time.ReadableInstant
@@ -42,6 +54,8 @@ class WelcomeViewModelTest {
MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial)
private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
+ private val purchaseResult = MutableStateFlow<PurchaseResult?>(null)
+ private val paymentAvailability = MutableStateFlow<PaymentAvailability?>(null)
// Service connections
private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
@@ -50,15 +64,17 @@ class WelcomeViewModelTest {
// Event notifiers
private val eventNotifierTunnelUiState = EventNotifier<TunnelState>(TunnelState.Disconnected)
- private val mockAccountRepository: AccountRepository = mockk()
+ private val mockAccountRepository: AccountRepository = mockk(relaxed = true)
private val mockDeviceRepository: DeviceRepository = mockk()
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
+ private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
private lateinit var viewModel: WelcomeViewModel
@Before
fun setUp() {
mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
+ mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
every { mockDeviceRepository.deviceState } returns deviceState
@@ -70,11 +86,16 @@ class WelcomeViewModelTest {
every { mockAccountRepository.accountExpiryState } returns accountExpiryState
+ coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResult
+
+ coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
+
viewModel =
WelcomeViewModel(
accountRepository = mockAccountRepository,
deviceRepository = mockDeviceRepository,
serviceConnectionManager = mockServiceConnectionManager,
+ paymentUseCase = mockPaymentUseCase,
pollAccountExpiry = false
)
}
@@ -112,9 +133,9 @@ class WelcomeViewModelTest {
// Act, Assert
viewModel.uiState.test {
assertEquals(WelcomeUiState(), awaitItem())
+ eventNotifierTunnelUiState.notify(tunnelUiStateTestItem)
serviceConnectionState.value =
ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelUiState.notify(tunnelUiStateTestItem)
val result = awaitItem()
assertEquals(tunnelUiStateTestItem, result.tunnelState)
}
@@ -158,8 +179,115 @@ class WelcomeViewModelTest {
}
}
+ @Test
+ fun testBillingProductsUnavailableState() = runTest {
+ // Arrange
+ val productsUnavailable = PaymentAvailability.ProductsUnavailable
+
+ // Act, Assert
+ viewModel.uiState.test {
+ // Default item
+ awaitItem()
+ paymentAvailability.tryEmit(productsUnavailable)
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.NoPayment>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsGenericErrorState() = runTest {
+ // Arrange
+ val paymentOtherError = PaymentAvailability.Error.Other(mockk())
+ paymentAvailability.tryEmit(paymentOtherError)
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Generic>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsBillingErrorState() = runTest {
+ // Arrange
+ val paymentBillingError = PaymentAvailability.Error.BillingUnavailable
+ paymentAvailability.value = paymentBillingError
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.Error.Billing>(result)
+ }
+ }
+
+ @Test
+ fun testBillingProductsPaymentAvailableState() = runTest {
+ // Arrange
+ val mockProduct: PaymentProduct = mockk()
+ val expectedProductList = listOf(mockProduct)
+ val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct))
+ paymentAvailability.value = productsAvailable
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+
+ // Act, Assert
+ viewModel.uiState.test {
+ val result = awaitItem().billingPaymentState
+ assertIs<PaymentState.PaymentAvailable>(result)
+ assertLists(expectedProductList, result.products)
+ }
+ }
+
+ @Test
+ fun testBillingUserCancelled() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Cancelled
+ purchaseResult.value = result
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ every { result.toPaymentDialogData() } returns null
+
+ // Act, Assert
+ viewModel.uiState.test { assertNull(awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testBillingPurchaseSuccess() = runTest {
+ // Arrange
+ val result = PurchaseResult.Completed.Success
+ val expectedData: PaymentDialogData = mockk()
+ purchaseResult.value = result
+ serviceConnectionState.value =
+ ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ every { result.toPaymentDialogData() } returns expectedData
+
+ // Act, Assert
+ viewModel.uiState.test { assertEquals(expectedData, awaitItem().paymentDialogData) }
+ }
+
+ @Test
+ fun testStartBillingPayment() {
+ // Arrange
+ val mockProductId = ProductId("MOCK")
+ val mockActivityProvider = mockk<() -> Activity>()
+
+ // Act
+ viewModel.startBillingPayment(mockProductId, mockActivityProvider)
+
+ // Assert
+ coVerify { mockPaymentUseCase.purchaseProduct(mockProductId, mockActivityProvider) }
+ }
+
companion object {
private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
"net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
+ private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
+ "net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
}
}