test(upgrade): Cover PENDING purchases granting no Pro and no ack

This commit is contained in:
Matthias Urhahn
2026-07-14 17:27:36 +02:00
committed by GitHub
parent f4facd3e86
commit bfc0e1cfaa
2 changed files with 61 additions and 0 deletions
@@ -6,17 +6,40 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.coroutine.runTest2
class BillingClientConnectionTest : BaseTest() {
private fun mockPurchase(
productId: String = CapodSku.Iap.PRO_UPGRADE.id,
purchaseTime: Long = 1_000,
state: Int = Purchase.PurchaseState.PURCHASED,
): Purchase = mockk {
every { products } returns listOf(productId)
every { this@mockk.purchaseTime } returns purchaseTime
every { purchaseState } returns state
}
@Test
fun `pending purchases are filtered out of the push-based purchases flow`() = runTest2 {
// A PENDING purchase (e.g. a slow cash/deferred payment) must never reach the entitlement
// layer — only PURCHASED grants Pro. This guards the #628 fix at the connection boundary.
val pending = mockPurchase(purchaseTime = 2_000, state = Purchase.PurchaseState.PENDING)
val purchased = mockPurchase(purchaseTime = 1_000, state = Purchase.PurchaseState.PURCHASED)
val connection = BillingClientConnection(
client = mockk(relaxed = true),
purchasesGlobal = MutableStateFlow(listOf(pending, purchased)),
freshObservations = MutableSharedFlow(),
purchaseFailuresGlobal = MutableSharedFlow(),
)
connection.purchases.first() shouldBe listOf(purchased)
}
@Test
@@ -2,6 +2,7 @@ package eu.darken.capod.common.upgrade.core.data
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.Purchase
import eu.darken.capod.common.AppForegroundState
import eu.darken.capod.common.upgrade.core.client.BillingClientConnection
import eu.darken.capod.common.upgrade.core.client.BillingClientConnectionProvider
@@ -212,6 +213,43 @@ class BillingDataRepoTest : BaseTest() {
mapped shouldBe original
}
private fun mockPurchase(state: Int, acknowledged: Boolean): Purchase = mockk {
every { purchaseState } returns state
every { isAcknowledged } returns acknowledged
}
@Test
fun `ack pipeline acknowledges only unacknowledged PURCHASED purchases`() = runTest2 {
val testScope = TestScope(UnconfinedTestDispatcher(testScheduler))
// A PENDING purchase must not be acknowledged (acking it fails and spins the retry loop);
// an already-acknowledged one must not be re-acked; only a settled, unacked one is acked.
val pending = mockPurchase(Purchase.PurchaseState.PENDING, acknowledged = false)
val purchasedUnacked = mockPurchase(Purchase.PurchaseState.PURCHASED, acknowledged = false)
val purchasedAcked = mockPurchase(Purchase.PurchaseState.PURCHASED, acknowledged = true)
val clientConnection = mockk<BillingClientConnection> {
every { purchases } returns flowOf(listOf(pending, purchasedUnacked, purchasedAcked))
coEvery { acknowledgePurchase(any()) } returns Unit
}
val provider = mockk<BillingClientConnectionProvider> {
every { connection } returns flowOf(clientConnection)
}
val foregroundState = mockk<AppForegroundState> {
every { isForeground } returns MutableStateFlow(false)
}
try {
BillingDataRepo(provider, testScope, foregroundState, TestTimeSource())
testScope.testScheduler.runCurrent()
coVerify(exactly = 1) { clientConnection.acknowledgePurchase(purchasedUnacked) }
coVerify(exactly = 0) { clientConnection.acknowledgePurchase(pending) }
coVerify(exactly = 0) { clientConnection.acknowledgePurchase(purchasedAcked) }
} finally {
testScope.cancel()
}
}
private class ForegroundRefreshHarness(testScope: TestScope) {
val clientConnection = mockk<BillingClientConnection> {
every { purchases } returns emptyFlow()