diff --git a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModel.kt b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModel.kt index 8df49cd62..a1ea003af 100644 --- a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModel.kt +++ b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModel.kt @@ -18,7 +18,6 @@ package me.proton.core.payment.presentation.viewmodel -import java.util.Optional import android.app.Activity import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -52,7 +51,7 @@ import me.proton.core.payment.domain.usecase.PaymentProvider.GoogleInAppPurchase import me.proton.core.payment.presentation.LogTag import me.proton.core.payment.presentation.viewmodel.ProtonPaymentEvent.Error import me.proton.core.plan.domain.entity.DynamicPlan -import me.proton.core.plan.domain.entity.SubscriptionManagement +import me.proton.core.plan.domain.entity.SubscriptionManagement.GOOGLE_MANAGED import me.proton.core.plan.domain.usecase.GetDynamicSubscription import me.proton.core.plan.domain.usecase.PerformGiapPurchase import me.proton.core.presentation.app.ActivityProvider @@ -60,6 +59,7 @@ import me.proton.core.presentation.viewmodel.ProtonViewModel import me.proton.core.util.kotlin.CoreLogger import me.proton.core.util.kotlin.coroutine.ResultCollector import me.proton.core.util.kotlin.coroutine.launchWithResultContext +import java.util.Optional import javax.inject.Inject import kotlin.jvm.optionals.getOrNull @@ -106,12 +106,15 @@ internal class ProtonPaymentButtonViewModel @Inject constructor( val lastEvent = flow { emit(ProtonPaymentEvent.Loading) - val subscription = userId?.let { getCurrentSubscription(it) } - if (subscription?.external != null && subscription.external == SubscriptionManagement.GOOGLE_MANAGED - && subscription.deeplink != null - ) { - emit(Error.SubscriptionManagedByOtherApp(userId, subscription.deeplink!!)) - return@flow + if (userId != null) { + val subscription = getCurrentSubscription(userId) + if (subscription == null) { + emit(Error.Generic(Exception("Could not get current subscription."))) + return@flow + } else if (subscription.external == GOOGLE_MANAGED && subscription.deeplink != null) { + emit(Error.SubscriptionManagedByOtherApp(userId, subscription.deeplink!!)) + return@flow + } } when (resolvedPaymentProvider) { diff --git a/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModelTest.kt b/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModelTest.kt index 530b23a31..ec6b260ef 100644 --- a/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModelTest.kt +++ b/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/ProtonPaymentButtonViewModelTest.kt @@ -27,9 +27,8 @@ import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.launch +import me.proton.core.domain.entity.UserId import me.proton.core.observability.domain.ObservabilityManager -import me.proton.core.observability.domain.metrics.CheckoutBillingSubscribeTotal -import me.proton.core.observability.domain.metrics.CheckoutGiapBillingCreatePaymentTokenTotal import me.proton.core.observability.domain.metrics.CheckoutGiapBillingLaunchBillingTotal import me.proton.core.observability.domain.metrics.CheckoutGiapBillingProductQueryTotal import me.proton.core.observability.domain.metrics.CheckoutGiapBillingQuerySubscriptionsTotal @@ -44,6 +43,7 @@ import me.proton.core.payment.domain.usecase.GetPreferredPaymentProvider import me.proton.core.payment.domain.usecase.PaymentProvider import me.proton.core.payment.presentation.viewmodel.ProtonPaymentButtonViewModel.ButtonState import me.proton.core.plan.domain.entity.DynamicPlan +import me.proton.core.plan.domain.entity.DynamicSubscription import me.proton.core.plan.domain.usecase.GetDynamicSubscription import me.proton.core.plan.domain.usecase.PerformGiapPurchase import me.proton.core.presentation.app.ActivityProvider @@ -82,6 +82,7 @@ class ProtonPaymentButtonViewModelTest : CoroutinesTest by CoroutinesTest() { fun setUp() { MockKAnnotations.init(this) convertToObservabilityGiapStatus = FakeConvertToObservabilityGiapStatus() + coEvery { getCurrentSubscription(any()) } returns mockk() tested = ProtonPaymentButtonViewModel( activityProvider, Optional.of(convertToObservabilityGiapStatus), @@ -337,6 +338,23 @@ class ProtonPaymentButtonViewModelTest : CoroutinesTest by CoroutinesTest() { ) } } + + @Test + fun `subscription is null`() = coroutinesTest { + // GIVEN + coEvery { getCurrentSubscription(any()) } returns null + val events = tested.paymentEvents(1) + + events.test { + // WHEN + tested.onPayClicked(1, "CHF", 12, PaymentProvider.CardPayment, mockk(), UserId("uid")).join() + + // THEN + assertEquals(ProtonPaymentEvent.Loading, awaitItem()) + val err = assertIs(awaitItem()) + assertEquals("Could not get current subscription.", err.throwable.message) + } + } } private class FakeConvertToObservabilityGiapStatus : ConvertToObservabilityGiapStatus { diff --git a/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobile.kt b/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobile.kt index 2818f3cff..699a47955 100644 --- a/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobile.kt +++ b/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobile.kt @@ -25,8 +25,6 @@ import me.proton.core.payment.domain.usecase.GoogleServicesUtils import me.proton.core.payment.domain.usecase.PaymentProvider import me.proton.core.plan.domain.SupportUpgradePaidPlans import me.proton.core.plan.domain.entity.SubscriptionManagement -import me.proton.core.user.domain.UserManager -import me.proton.core.user.domain.extension.canReadSubscription import java.util.Optional import javax.inject.Inject import kotlin.jvm.optionals.getOrNull @@ -36,17 +34,13 @@ class CanUpgradeFromMobile @Inject constructor( private val getAvailablePaymentProviders: GetAvailablePaymentProviders, private val getCurrentSubscription: GetDynamicSubscription, private val googleServicesUtils: Optional, - private val userManager: UserManager ) { suspend operator fun invoke(userId: UserId): Boolean { if (!supportPaidPlans) { return false } - if (!userManager.getUser(userId).canReadSubscription()) { - return false - } - val subscription = getCurrentSubscription(userId) + val subscription = getCurrentSubscription(userId) ?: return false if (subscription.external != null && subscription.external != SubscriptionManagement.GOOGLE_MANAGED) { return false } diff --git a/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscription.kt b/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscription.kt index 2fd240901..e38873147 100644 --- a/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscription.kt +++ b/plan/domain/src/main/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscription.kt @@ -19,21 +19,26 @@ package me.proton.core.plan.domain.usecase import me.proton.core.domain.entity.UserId -import me.proton.core.payment.domain.repository.PaymentsRepository import me.proton.core.plan.domain.entity.DynamicSubscription import me.proton.core.plan.domain.repository.PlansRepository +import me.proton.core.user.domain.UserManager +import me.proton.core.user.domain.extension.canReadSubscription +import me.proton.core.util.kotlin.runCatchingCheckedExceptions import javax.inject.Inject /** * Gets current active dynamic subscription a user has. * Authorized. This means that it could only be used for upgrades. New accounts created during sign ups logically do not * have existing subscriptions. - * NOTE: You may want to call [me.proton.core.user.domain.extension.canReadSubscription] before calling this. */ public class GetDynamicSubscription @Inject constructor( - private val plansRepository: PlansRepository + private val plansRepository: PlansRepository, + private val userManager: UserManager ) { - public suspend operator fun invoke(userId: UserId): DynamicSubscription { - return plansRepository.getDynamicSubscriptions(userId).first() - } + public suspend operator fun invoke(userId: UserId): DynamicSubscription? = runCatchingCheckedExceptions { + when { + userManager.getUser(userId).canReadSubscription() -> plansRepository.getDynamicSubscriptions(userId).first() + else -> null + } + }.getOrNull() } diff --git a/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobileTest.kt b/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobileTest.kt index f745e5c5f..4ea8da070 100644 --- a/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobileTest.kt +++ b/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/CanUpgradeFromMobileTest.kt @@ -44,7 +44,7 @@ class CanUpgradeFromMobileTest { @MockK private lateinit var getAvailablePaymentProviders: GetAvailablePaymentProviders - @MockK(relaxed = true) + @MockK private lateinit var getCurrentSubscription: GetDynamicSubscription @MockK @@ -53,9 +53,6 @@ class CanUpgradeFromMobileTest { @MockK private lateinit var optionalGoogleServicesUtils: Optional - @MockK - private lateinit var userManager: UserManager - private val testUserId = UserId("user-id") private lateinit var useCase: CanUpgradeFromMobile @@ -66,8 +63,7 @@ class CanUpgradeFromMobileTest { supportPaidPlans = true, getAvailablePaymentProviders = getAvailablePaymentProviders, getCurrentSubscription = getCurrentSubscription, - googleServicesUtils = optionalGoogleServicesUtils, - userManager = userManager + googleServicesUtils = optionalGoogleServicesUtils ) } @@ -78,8 +74,7 @@ class CanUpgradeFromMobileTest { supportPaidPlans = false, getAvailablePaymentProviders = getAvailablePaymentProviders, getCurrentSubscription = getCurrentSubscription, - googleServicesUtils = optionalGoogleServicesUtils, - userManager = userManager + googleServicesUtils = optionalGoogleServicesUtils ) // WHEN val result = useCase(testUserId) @@ -91,7 +86,8 @@ class CanUpgradeFromMobileTest { fun `can upgrade returns false when no payment providers available`() = runTest { // GIVEN coEvery { getAvailablePaymentProviders() } returns emptySet() - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } + coEvery { getCurrentSubscription(testUserId) } returns + mockk { every { external } returns SubscriptionManagement.GOOGLE_MANAGED } // WHEN val result = useCase(testUserId) // THEN @@ -102,7 +98,8 @@ class CanUpgradeFromMobileTest { fun `can upgrade returns false when only PayPal payment provider is available`() = runTest { // GIVEN coEvery { getAvailablePaymentProviders() } returns setOf(PaymentProvider.PayPal) - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } + coEvery { getCurrentSubscription(testUserId) } returns + mockk { every { external } returns SubscriptionManagement.GOOGLE_MANAGED } // WHEN val result = useCase(testUserId) // THEN @@ -114,14 +111,12 @@ class CanUpgradeFromMobileTest { // GIVEN every { optionalGoogleServicesUtils.getOrNull() } returns googleServicesUtils every { googleServicesUtils.isGooglePlayServicesAvailable() } returns GoogleServicesAvailability.Success - coEvery { getCurrentSubscription(testUserId) } returns mockk { - every { external } returns SubscriptionManagement.GOOGLE_MANAGED - } + coEvery { getCurrentSubscription(testUserId) } returns + mockk { every { external } returns SubscriptionManagement.GOOGLE_MANAGED } coEvery { getAvailablePaymentProviders() } returns setOf( PaymentProvider.CardPayment, PaymentProvider.GoogleInAppPurchase ) - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } // WHEN val result = useCase(testUserId) // THEN @@ -133,13 +128,11 @@ class CanUpgradeFromMobileTest { // GIVEN every { optionalGoogleServicesUtils.getOrNull() } returns googleServicesUtils every { googleServicesUtils.isGooglePlayServicesAvailable() } returns GoogleServicesAvailability.ServiceInvalid - coEvery { getCurrentSubscription(testUserId) } returns mockk { - every { external } returns SubscriptionManagement.GOOGLE_MANAGED - } + coEvery { getCurrentSubscription(testUserId) } returns + mockk { every { external } returns SubscriptionManagement.GOOGLE_MANAGED } coEvery { getAvailablePaymentProviders() } returns setOf( PaymentProvider.GoogleInAppPurchase ) - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } // WHEN val result = useCase(testUserId) // THEN @@ -149,14 +142,12 @@ class CanUpgradeFromMobileTest { @Test fun `can upgrade returns false for Proton Managed when payment providers available`() = runTest { // GIVEN - coEvery { getCurrentSubscription(testUserId) } returns mockk { - every { external } returns SubscriptionManagement.PROTON_MANAGED - } + coEvery { getCurrentSubscription(testUserId) } returns + mockk { every { external } returns SubscriptionManagement.PROTON_MANAGED } coEvery { getAvailablePaymentProviders() } returns setOf( PaymentProvider.CardPayment, PaymentProvider.GoogleInAppPurchase ) - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } // WHEN val result = useCase(testUserId) // THEN @@ -164,9 +155,9 @@ class CanUpgradeFromMobileTest { } @Test - fun `can upgrade returns false for members of organizations`() = runTest { + fun `can upgrade returns false if subscription is null`() = runTest { // GIVEN - coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.OrganizationMember } + coEvery { getCurrentSubscription(testUserId) } returns null // WHEN val result = useCase(testUserId) // THEN diff --git a/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscriptionTest.kt b/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscriptionTest.kt index 586c468f5..e7b9c58c0 100644 --- a/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscriptionTest.kt +++ b/plan/domain/src/test/kotlin/me/proton/core/plan/domain/usecase/GetDynamicSubscriptionTest.kt @@ -18,7 +18,10 @@ package me.proton.core.plan.domain.usecase +import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.mockk import kotlinx.coroutines.test.runTest import me.proton.core.domain.entity.UserId @@ -27,15 +30,23 @@ import me.proton.core.network.domain.ApiResult import me.proton.core.network.domain.ResponseCodes import me.proton.core.plan.domain.entity.dynamicSubscription import me.proton.core.plan.domain.repository.PlansRepository +import me.proton.core.user.domain.UserManager +import me.proton.core.user.domain.entity.Role import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test +import kotlin.coroutines.cancellation.CancellationException import kotlin.test.assertFailsWith +import kotlin.test.assertNull class GetDynamicSubscriptionTest { // region mocks - private val repository = mockk(relaxed = true) + @MockK(relaxed = true) + private lateinit var repository: PlansRepository + + @MockK + private lateinit var userManager: UserManager // endregion // region test data @@ -48,19 +59,21 @@ class GetDynamicSubscriptionTest { @Before fun beforeEveryTest() { - useCase = GetDynamicSubscription(repository) + MockKAnnotations.init(this) + useCase = GetDynamicSubscription(repository, userManager) } @Test fun `get subscription returns success, enqueue getsubscription observability success`() = runTest { // GIVEN coEvery { repository.getDynamicSubscriptions(testUserId) } returns testSubscriptions + coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } // WHEN val result = useCase.invoke(testUserId) // THEN - assertEquals(testSubscription, result) assertNotNull(result) - assertEquals(0L, result.amount) + assertEquals(testSubscription, result) + assertEquals(0L, result?.amount) } @Test @@ -72,13 +85,11 @@ class GetDynamicSubscriptionTest { RuntimeException("Test error") ) ) + coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.OrganizationAdmin } // WHEN - val throwable = assertFailsWith(ApiException::class) { - useCase.invoke(testUserId) - } + val result = useCase.invoke(testUserId) // THEN - assertNotNull(throwable) - assertEquals("Test error", throwable.message) + assertNull(result) } @Test @@ -94,7 +105,28 @@ class GetDynamicSubscriptionTest { ) ) ) + coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.NoOrganization } // WHEN - assertFailsWith { useCase.invoke(testUserId) } + val result = useCase.invoke(testUserId) + assertNull(result) + } + + @Test + fun `no subscription if user is member of organization`() = runTest { + // GIVEN + coEvery { userManager.getUser(testUserId) } returns mockk { every { role } returns Role.OrganizationMember } + + // WHEN + val result = useCase.invoke(testUserId) + assertNull(result) + } + + @Test + fun `exception is rethrown for runtime exception`() = runTest { + // GIVEN + coEvery { userManager.getUser(testUserId) } throws CancellationException("Error") + + // WHEN + assertFailsWith { useCase.invoke(testUserId) } } } diff --git a/plan/presentation-compose/src/main/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModel.kt b/plan/presentation-compose/src/main/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModel.kt index 1c0bb1499..281e05c36 100644 --- a/plan/presentation-compose/src/main/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModel.kt +++ b/plan/presentation-compose/src/main/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import me.proton.core.compose.viewmodel.stopTimeoutMillis @@ -42,6 +43,7 @@ public class UpgradeStorageInfoViewModel @Inject constructor( ) : ProtonViewModel() { public val state: StateFlow = shouldUpgradeStorage() .map { it.toAccountStorageState() } + .catch { emit(Hidden) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis), INITIAL_STATE) internal companion object { diff --git a/plan/presentation-compose/src/test/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModelTest.kt b/plan/presentation-compose/src/test/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModelTest.kt index e35dce336..5d6bb173d 100644 --- a/plan/presentation-compose/src/test/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModelTest.kt +++ b/plan/presentation-compose/src/test/kotlin/me/proton/core/plan/presentation/compose/viewmodel/UpgradeStorageInfoViewModelTest.kt @@ -49,4 +49,14 @@ class UpgradeStorageInfoViewModelTest : CoroutinesTest by CoroutinesTest() { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `state is Hidden when exception is thrown`() = coroutinesTest { + every { shouldUpgradeStorage() } throws Exception() + + tested.state.test { + assertEquals(AccountStorageState.Hidden, awaitItem()) + expectNoEvents() + } + } } diff --git a/plan/presentation/src/main/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchase.kt b/plan/presentation/src/main/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchase.kt index 2a271f3bb..d23367b5e 100644 --- a/plan/presentation/src/main/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchase.kt +++ b/plan/presentation/src/main/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchase.kt @@ -66,7 +66,7 @@ internal class CheckUnredeemedGooglePurchase @Inject constructor( val findUnacknowledgedGooglePurchase = findUnacknowledgedGooglePurchase.getOrNull() ?: return null if (PaymentProvider.GoogleInAppPurchase !in getAvailablePaymentProviders()) return null - val subscription = getCurrentSubscription(userId) + val subscription = getCurrentSubscription(userId) ?: return null val subscriptionCustomerId = subscription.customerId val googlePurchases = if (subscriptionCustomerId != null) { listOfNotNull(findUnacknowledgedGooglePurchase.byCustomer(subscriptionCustomerId)) diff --git a/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchaseTest.kt b/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchaseTest.kt index f107ea57b..13af6eda2 100644 --- a/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchaseTest.kt +++ b/plan/presentation/src/test/kotlin/me/proton/core/plan/presentation/usecase/CheckUnredeemedGooglePurchaseTest.kt @@ -22,11 +22,8 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest -import me.proton.core.account.domain.entity.AccountType import me.proton.core.domain.entity.AppStore import me.proton.core.domain.entity.UserId -import me.proton.core.network.domain.ApiException -import me.proton.core.network.domain.ApiResult import me.proton.core.payment.domain.entity.GooglePurchase import me.proton.core.payment.domain.entity.ProductId import me.proton.core.payment.domain.usecase.FindUnacknowledgedGooglePurchase @@ -199,7 +196,7 @@ class CheckUnredeemedGooglePurchaseTest { coEvery { findUnacknowledgedGooglePurchase.byCustomer(customerA) } returns googlePurchase coEvery { getAvailablePaymentProviders.invoke() } returns PaymentProvider.entries.toSet() - coEvery { getCurrentSubscription.invoke(userId) } returns mockk { + coEvery { getCurrentSubscription.invoke(userId) } returns mockk { every { customerId } returns customerA every { external } returns SubscriptionManagement.PROTON_MANAGED } @@ -238,7 +235,7 @@ class CheckUnredeemedGooglePurchaseTest { coEvery { findUnacknowledgedGooglePurchase.byCustomer(any()) } returns googlePurchase coEvery { getAvailablePaymentProviders.invoke() } returns PaymentProvider.entries.toSet() - coEvery { getCurrentSubscription.invoke(userId) } returns mockk { + coEvery { getCurrentSubscription.invoke(userId) } returns mockk { every { customerId } returns customerB every { external } returns SubscriptionManagement.GOOGLE_MANAGED } @@ -279,7 +276,7 @@ class CheckUnredeemedGooglePurchaseTest { coEvery { findUnacknowledgedGooglePurchase.byCustomer(customerA) } returns googlePurchase coEvery { getAvailablePaymentProviders.invoke() } returns PaymentProvider.entries.toSet() - coEvery { getCurrentSubscription.invoke(userId) } returns mockk { + coEvery { getCurrentSubscription.invoke(userId) } returns mockk { every { customerId } returns customerA every { external } returns SubscriptionManagement.GOOGLE_MANAGED every { name } returns planB @@ -318,7 +315,7 @@ class CheckUnredeemedGooglePurchaseTest { coEvery { findUnacknowledgedGooglePurchase.byCustomer(customerA) } returns googlePurchase coEvery { getAvailablePaymentProviders.invoke() } returns PaymentProvider.entries.toSet() - coEvery { getCurrentSubscription.invoke(userId) } returns mockk { + coEvery { getCurrentSubscription.invoke(userId) } returns mockk { every { customerId } returns "customer-A" every { external } returns SubscriptionManagement.GOOGLE_MANAGED every { name } returns "plan-A" @@ -444,9 +441,9 @@ class CheckUnredeemedGooglePurchaseTest { } @Test - fun `returns null on network error`() = runTest { + fun `returns null if null subscription`() = runTest { coEvery { getAvailablePaymentProviders.invoke() } returns PaymentProvider.entries.toSet() - coEvery { getCurrentSubscription.invoke(any()) } throws ApiException(ApiResult.Error.Connection()) + coEvery { getCurrentSubscription.invoke(any()) } returns null assertNull(tested(mockk())) } } diff --git a/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/RunCatching.kt b/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/RunCatching.kt new file mode 100644 index 000000000..a438a1f8a --- /dev/null +++ b/util/kotlin/src/main/kotlin/me/proton/core/util/kotlin/RunCatching.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.util.kotlin + +/** + * Runs the given block of code and returns the result. + * Any [RuntimeException] or [Error] are re-thrown + * (e.g. [kotlin.coroutines.cancellation.CancellationException] or [OutOfMemoryError]). + */ +@Suppress("TooGenericExceptionCaught") +inline fun runCatchingCheckedExceptions(block: () -> R): Result { + return try { + Result.success(block()) + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } +}