|
| 1 | +package dev.hyo.openiap |
| 2 | + |
| 3 | +import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener |
| 4 | +import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener |
| 5 | +import dev.hyo.openiap.models.* |
| 6 | +import dev.hyo.openiap.store.OpenIapStore |
| 7 | +import org.junit.Assert.* |
| 8 | +import org.junit.Test |
| 9 | + |
| 10 | +class OpenIapStoreTest { |
| 11 | + |
| 12 | + // Fake implementation of OpenIapProtocol for unit testing |
| 13 | + class FakeModule : OpenIapProtocol { |
| 14 | + var initCalled = 0 |
| 15 | + var endCalled = 0 |
| 16 | + var finishCalled = 0 |
| 17 | + var lastDeepLinkOptions: DeepLinkOptions? = null |
| 18 | + private val updateListeners = mutableSetOf<OpenIapPurchaseUpdateListener>() |
| 19 | + private val errorListeners = mutableSetOf<OpenIapPurchaseErrorListener>() |
| 20 | + |
| 21 | + // Configurable responses |
| 22 | + var productsToReturn: List<OpenIapProduct> = emptyList() |
| 23 | + var purchasesToReturn: List<OpenIapPurchase> = emptyList() |
| 24 | + var activeSubsToReturn: List<OpenIapActiveSubscription> = emptyList() |
| 25 | + var requestEmitsPurchases: List<OpenIapPurchase> = emptyList() |
| 26 | + |
| 27 | + override suspend fun initConnection(): Boolean { |
| 28 | + initCalled++ |
| 29 | + return true |
| 30 | + } |
| 31 | + |
| 32 | + override suspend fun endConnection(): Boolean { |
| 33 | + endCalled++ |
| 34 | + return true |
| 35 | + } |
| 36 | + |
| 37 | + override suspend fun fetchProducts(params: ProductRequest): List<OpenIapProduct> = productsToReturn |
| 38 | + |
| 39 | + override suspend fun getAvailablePurchases(options: PurchaseOptions?): List<OpenIapPurchase> = purchasesToReturn |
| 40 | + |
| 41 | + override suspend fun getActiveSubscriptions(subscriptionIds: List<String>?): List<OpenIapActiveSubscription> = activeSubsToReturn |
| 42 | + |
| 43 | + override suspend fun hasActiveSubscriptions(subscriptionIds: List<String>?): Boolean = activeSubsToReturn.isNotEmpty() |
| 44 | + |
| 45 | + override suspend fun requestPurchase(request: RequestPurchaseAndroidProps, type: ProductRequest.ProductRequestType): List<OpenIapPurchase> { |
| 46 | + // Broadcast to listeners |
| 47 | + requestEmitsPurchases.forEach { p -> |
| 48 | + updateListeners.forEach { it.onPurchaseUpdated(p) } |
| 49 | + } |
| 50 | + return requestEmitsPurchases |
| 51 | + } |
| 52 | + |
| 53 | + override suspend fun finishTransaction(params: FinishTransactionParams): PurchaseResult { |
| 54 | + finishCalled++ |
| 55 | + return PurchaseResult(responseCode = 0) |
| 56 | + } |
| 57 | + |
| 58 | + override suspend fun validateReceipt(sku: String, androidOptions: ReceiptValidationProps.AndroidValidationOptions?): ReceiptValidationResultAndroid? = null |
| 59 | + |
| 60 | + override suspend fun validateReceipt(options: ReceiptValidationProps): ReceiptValidationResultAndroid? = null |
| 61 | + |
| 62 | + override suspend fun acknowledgePurchaseAndroid(purchaseToken: String) {} |
| 63 | + |
| 64 | + override suspend fun consumePurchaseAndroid(purchaseToken: String) {} |
| 65 | + |
| 66 | + override suspend fun flushFailedPurchaseCachedAsPendingAndroid() {} |
| 67 | + |
| 68 | + override suspend fun deepLinkToSubscriptions(options: DeepLinkOptions) { |
| 69 | + lastDeepLinkOptions = options |
| 70 | + } |
| 71 | + |
| 72 | + override fun addPurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) { |
| 73 | + updateListeners.add(listener) |
| 74 | + } |
| 75 | + |
| 76 | + override fun removePurchaseUpdateListener(listener: OpenIapPurchaseUpdateListener) { |
| 77 | + updateListeners.remove(listener) |
| 78 | + } |
| 79 | + |
| 80 | + override fun addPurchaseErrorListener(listener: OpenIapPurchaseErrorListener) { |
| 81 | + errorListeners.add(listener) |
| 82 | + } |
| 83 | + |
| 84 | + override fun removePurchaseErrorListener(listener: OpenIapPurchaseErrorListener) { |
| 85 | + errorListeners.remove(listener) |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + private fun samplePurchase(token: String = "token-1", productId: String = "sku1"): OpenIapPurchase { |
| 90 | + return OpenIapPurchase( |
| 91 | + id = token, |
| 92 | + productId = productId, |
| 93 | + ids = listOf(productId), |
| 94 | + transactionId = "order-1", |
| 95 | + transactionDate = System.currentTimeMillis(), |
| 96 | + transactionReceipt = "{}", |
| 97 | + purchaseToken = token, |
| 98 | + platform = "android", |
| 99 | + quantity = 1, |
| 100 | + purchaseState = OpenIapPurchase.PurchaseState.PURCHASED, |
| 101 | + isAutoRenewing = false, |
| 102 | + purchaseTokenAndroid = token, |
| 103 | + dataAndroid = null, |
| 104 | + signatureAndroid = null, |
| 105 | + autoRenewingAndroid = false, |
| 106 | + isAcknowledgedAndroid = false, |
| 107 | + packageNameAndroid = "dev.hyo.martie", |
| 108 | + developerPayloadAndroid = "", |
| 109 | + obfuscatedAccountIdAndroid = "", |
| 110 | + obfuscatedProfileIdAndroid = "", |
| 111 | + ) |
| 112 | + } |
| 113 | + |
| 114 | + @Test |
| 115 | + fun finishTransaction_isIdempotentByToken() = kotlinx.coroutines.test.runTest { |
| 116 | + val fake = FakeModule() |
| 117 | + val store = OpenIapStore(fake) |
| 118 | + val purchase = samplePurchase(token = "t-123", productId = "sku.test") |
| 119 | + |
| 120 | + val first = store.finishTransaction(purchase, isConsumable = false) |
| 121 | + val second = store.finishTransaction(purchase, isConsumable = false) |
| 122 | + |
| 123 | + assertTrue(first) |
| 124 | + assertTrue(second) |
| 125 | + assertEquals(1, fake.finishCalled) |
| 126 | + } |
| 127 | + |
| 128 | + @Test |
| 129 | + fun requestPurchase_emitsListenerUpdates_andReturnsList() = kotlinx.coroutines.test.runTest { |
| 130 | + val fake = FakeModule() |
| 131 | + val store = OpenIapStore(fake) |
| 132 | + val emitted = samplePurchase(token = "t-1", productId = "sku1") |
| 133 | + fake.requestEmitsPurchases = listOf(emitted) |
| 134 | + |
| 135 | + val result = store.requestPurchase( |
| 136 | + RequestPurchaseAndroidProps(skus = listOf("sku1")), |
| 137 | + ProductRequest.ProductRequestType.INAPP |
| 138 | + ) |
| 139 | + |
| 140 | + assertEquals(1, result.size) |
| 141 | + assertEquals("sku1", result.first().productId) |
| 142 | + assertEquals("sku1", store.currentPurchase.value?.productId) |
| 143 | + } |
| 144 | + |
| 145 | + @Test |
| 146 | + fun deepLink_isDelegatedToModule() = kotlinx.coroutines.test.runTest { |
| 147 | + val fake = FakeModule() |
| 148 | + val store = OpenIapStore(fake) |
| 149 | + |
| 150 | + val opts = DeepLinkOptions(skuAndroid = "skuX", packageNameAndroid = "dev.hyo.martie") |
| 151 | + store.deepLinkToSubscriptions(opts) |
| 152 | + assertEquals("skuX", fake.lastDeepLinkOptions?.skuAndroid) |
| 153 | + assertEquals("dev.hyo.martie", fake.lastDeepLinkOptions?.packageNameAndroid) |
| 154 | + } |
| 155 | + |
| 156 | + @Test |
| 157 | + fun getActiveSubscriptions_passThrough() = kotlinx.coroutines.test.runTest { |
| 158 | + val fake = FakeModule() |
| 159 | + val store = OpenIapStore(fake) |
| 160 | + val sub = OpenIapActiveSubscription( |
| 161 | + productId = "sku.sub", |
| 162 | + isActive = true, |
| 163 | + transactionId = "t", |
| 164 | + purchaseToken = "pt", |
| 165 | + transactionDate = System.currentTimeMillis(), |
| 166 | + platform = "android", |
| 167 | + autoRenewingAndroid = true |
| 168 | + ) |
| 169 | + fake.activeSubsToReturn = listOf(sub) |
| 170 | + |
| 171 | + val result = store.getActiveSubscriptions(listOf("sku.sub")) |
| 172 | + assertEquals(1, result.size) |
| 173 | + assertEquals("sku.sub", result.first().productId) |
| 174 | + } |
| 175 | +} |
| 176 | + |
0 commit comments