From c12b35e0544999694ebfce35f36b483ef6dcb3f9 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Mon, 3 Apr 2023 13:16:22 +0300 Subject: [PATCH 1/7] Integration tests for QonversionRepository. (#470) * Integration tests for QonversionRepository. * Small fix * Integration tests for QonversionRepository. * Detekt fixes. * Detekt fixes. * Uid fix. * Making all tests independent * CR fixes * CR fixes --------- Co-authored-by: GitHub Action --- .../qonversion/android/app/HomeFragment.kt | 1 - config/detekt/baseline.xml | 9 +- sdk/build.gradle | 12 + .../QonversionRepositoryIntegrationTest.kt | 589 ++++++++++++++++++ .../android/sdk/dto/offerings/QOffering.kt | 17 + .../android/sdk/dto/offerings/QOfferings.kt | 13 + .../sdk/internal/QonversionRepository.kt | 24 +- .../android/sdk/internal/extensions.kt | 3 + 8 files changed, 661 insertions(+), 7 deletions(-) create mode 100644 sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt diff --git a/app/src/main/java/com/qonversion/android/app/HomeFragment.kt b/app/src/main/java/com/qonversion/android/app/HomeFragment.kt index e6d19a047..8250ef311 100644 --- a/app/src/main/java/com/qonversion/android/app/HomeFragment.kt +++ b/app/src/main/java/com/qonversion/android/app/HomeFragment.kt @@ -28,7 +28,6 @@ import com.qonversion.android.sdk.dto.products.QProduct import com.qonversion.android.sdk.listeners.QEntitlementsUpdateListener import com.qonversion.android.sdk.listeners.QonversionEntitlementsCallback import com.qonversion.android.sdk.listeners.QonversionProductsCallback -import com.qonversion.android.sdk.listeners.QonversionShowScreenCallback private const val TAG = "HomeFragment" diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 23e691e83..9ec7ee13e 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -162,6 +162,10 @@ MaxLineLength:QonversionConfig.kt$QonversionConfig.Builder$* MaxLineLength:QonversionError.kt$QonversionErrorCode$* MaxLineLength:QonversionInternal.kt$QonversionInternal$val isHistoricalDataSynced: Boolean = sharedPreferencesCache?.getBool(Constants.IS_HISTORICAL_DATA_SYNCED) ?: false + MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" + MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$detailsToken = "AEuhp4Kd9cZ3ZlkS2MylEXHBcZVLjwwllncPBm4a6lrVvj3uYGICnsE5w87i81qNsa38DPOW08BcZfLxJFxIWeISVwoBkT55tA2Bb6cKGsip724=" + MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" + MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" MaxLineLength:ScreenPresenterTest.kt$ScreenPresenterTest$fun MaxLineLength:SharedPreferencesCacheTest.kt$SharedPreferencesCacheTest.Object${ Assert.assertEquals("Wrong available offerings size value", 1, realValue?.offerings?.availableOfferings?.size) } MaxLineLength:SharedPreferencesCacheTest.kt$SharedPreferencesCacheTest.Object${ Assert.assertEquals("Wrong experimentInfo value", expectedValue.offerings?.main?.experimentInfo, realValue?.offerings?.main?.experimentInfo) } @@ -190,6 +194,10 @@ MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:159 MaximumLineLength:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:172 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionInternal.kt:114 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:123 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:259 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:439 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:99 MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:117 MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:118 MaximumLineLength:com.qonversion.android.sdk.internal.billing.QonversionBillingService.kt:109 @@ -304,7 +312,6 @@ ReturnCount:QProductCenterManager.kt$QProductCenterManager$private fun processPurchase( context: Activity, productId: String, oldProductId: String?, @BillingFlowParams.ProrationMode prorationMode: Int?, offeringId: String?, callback: QonversionEntitlementsCallback ) ReturnCount:ScreenPresenter.kt$ScreenPresenter$override fun shouldOverrideUrlLoading(url: String?): Boolean ReturnCount:SkuDetailsTokenExtractor.kt$SkuDetailsTokenExtractor$override fun extract(response: String?): String - SpacingAroundColon:com.qonversion.android.sdk.internal.QonversionInternal.kt:119 SpacingAroundColon:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:45 SpacingAroundComma:com.qonversion.android.sdk.internal.converter.GooglePurchaseConverterTest.kt:146 SpacingAroundCurly:com.qonversion.android.sdk.automations.internal.QAutomationsManagerTest.kt:263 diff --git a/sdk/build.gradle b/sdk/build.gradle index 8e7d00335..05254e86c 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -13,6 +13,9 @@ android { group = 'io.qonversion.android.sdk' version = release.versionName versionName = release.versionName + + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } kotlinOptions { @@ -144,8 +147,11 @@ dependencies { // Kotlin Reflect implementation "org.jetbrains.kotlin:kotlin-reflect" + implementation 'com.android.support:multidex:1.0.3' + // JUnit testImplementation 'junit:junit:4.13' + androidTestImplementation 'junit:junit:4.13' testImplementation junit.api testRuntimeOnly junit.engine @@ -154,14 +160,20 @@ dependencies { // Robolectric testImplementation "org.robolectric:robolectric:4.3.1" + androidTestImplementation "org.robolectric:robolectric:4.4" // MockK testImplementation 'io.mockk:mockk:1.10.0' + androidTestImplementation 'io.mockk:mockk-android:1.10.0' // Mockito testImplementation 'org.mockito:mockito-core:2.23.0' testImplementation 'androidx.test:core:1.2.0' + androidTestImplementation 'androidx.test:core:1.4.0' + androidTestImplementation "androidx.test:runner:1.4.0" + androidTestImplementation "androidx.test:rules:1.4.0" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' testImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt new file mode 100644 index 000000000..33d32ba3d --- /dev/null +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt @@ -0,0 +1,589 @@ +package com.qonversion.android.sdk.internal + +import android.os.Handler +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.collect.Maps +import com.qonversion.android.sdk.QonversionConfig +import com.qonversion.android.sdk.dto.QAttributionProvider +import com.qonversion.android.sdk.dto.QEntitlementSource +import com.qonversion.android.sdk.dto.QLaunchMode +import com.qonversion.android.sdk.dto.QUserProperty +import com.qonversion.android.sdk.dto.QonversionError +import com.qonversion.android.sdk.dto.eligibility.QEligibility +import com.qonversion.android.sdk.dto.eligibility.QIntroEligibilityStatus +import com.qonversion.android.sdk.dto.offerings.QOffering +import com.qonversion.android.sdk.dto.offerings.QOfferingTag +import com.qonversion.android.sdk.dto.offerings.QOfferings +import com.qonversion.android.sdk.dto.products.QProduct +import com.qonversion.android.sdk.dto.products.QProductDuration +import com.qonversion.android.sdk.dto.products.QProductType +import com.qonversion.android.sdk.internal.di.QDependencyInjector +import com.qonversion.android.sdk.internal.dto.QLaunchResult +import com.qonversion.android.sdk.internal.dto.QPermission +import com.qonversion.android.sdk.internal.dto.QProductRenewState +import com.qonversion.android.sdk.internal.dto.purchase.History +import com.qonversion.android.sdk.internal.dto.request.data.InitRequestData +import com.qonversion.android.sdk.internal.provider.AppStateProvider +import com.qonversion.android.sdk.internal.purchase.Purchase +import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback +import com.qonversion.android.sdk.listeners.QonversionLaunchCallback +import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date +import java.util.concurrent.CountDownLatch + +private val uidPrefix = "QON_test_uid_android_" + System.currentTimeMillis() + +@RunWith(AndroidJUnit4::class) +internal class QonversionRepositoryIntegrationTest { + + private val appStateProvider = object : AppStateProvider { + override val appState: AppState + get() = AppState.Foreground + } + + private val installDate = 1679652674L + + private val noCodeScreenId = "lsarjYcU" + + private val monthlyProduct = QProduct( + "test_monthly", + "google_monthly", + QProductType.Subscription, + QProductDuration.Monthly + ) + private val annualProduct = + QProduct("test_annual", "google_annual", QProductType.Trial, QProductDuration.Annual) + private val inappProduct = QProduct("test_inapp", "google_inapp", QProductType.InApp, null) + private val expectedProducts = mapOf( + monthlyProduct.qonversionID to monthlyProduct, + annualProduct.qonversionID to annualProduct, + inappProduct.qonversionID to inappProduct + ) + + private val expectedOffering = QOffering( + "main", + QOfferingTag.Main, + listOf(annualProduct, monthlyProduct) + ) + + private val expectedOfferings = QOfferings( + expectedOffering, + listOf(expectedOffering) + ) + + private val expectedProductPermissions = mapOf( + "test_monthly" to listOf("premium"), + "test_annual" to listOf("premium"), + "test_inapp" to listOf("noAds") + ) + + private val expectedPermissions = mapOf( + "premium" to QPermission( + "premium", + "test_monthly", + QProductRenewState.Canceled, + Date(1679933171000), + Date(1679935273000), + QEntitlementSource.PlayStore, + 0 + ) + ) + + private val purchase = Purchase( + detailsToken = "AEuhp4Kd9cZ3ZlkS2MylEXHBcZVLjwwllncPBm4a6lrVvj3uYGICnsE5w87i81qNsa38DPOW08BcZfLxJFxIWeISVwoBkT55tA2Bb6cKGsip724=", + title = "DONT CHANGE! Sub for integration tests. (Qonversion Sample)", + description = "", + productId = "google_monthly", + type = "subs", + originalPrice = "$6.99", + originalPriceAmountMicros = 6990000, + priceCurrencyCode = "SGD", + price = "6.99", + priceAmountMicros = 6990000, + periodUnit = 2, + periodUnitsCount = 1, + freeTrialPeriod = "", + introductoryAvailable = false, + introductoryPriceAmountMicros = 0, + introductoryPrice = "0.00", + introductoryPriceCycles = 0, + introductoryPeriodUnit = 0, + introductoryPeriodUnitsCount = null, + orderId = "GPA.3307-0767-0668-99058", + originalOrderId = "GPA.3307-0767-0668-99058", + packageName = "com.qonversion.sample", + purchaseTime = 1679933171, + purchaseState = 1, + purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY", + acknowledged = false, + autoRenewing = true, + paymentMode = 0 + ) + + @Test + fun init() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_init" + val data = InitRequestData( + installDate, + null, + emptyList(), + object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue(Maps.difference(expectedProducts, launchResult.products).areEqual()) + assertTrue(Maps.difference(emptyMap(), launchResult.permissions).areEqual()) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + expectedProductPermissions, + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + ) + + val repository = initRepositoryForUid(uid) + + // when + repository.init(data) + + signal.await() + } + + @Test + fun purchase() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_purchase" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue(Maps.difference(expectedProducts, launchResult.products).areEqual()) + assertTrue(Maps.difference(emptyMap(), launchResult.permissions).areEqual()) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + emptyMap(), + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.purchase( + installDate, + purchase, + null, + "test_monthly", + callback + ) + } + + signal.await() + } + + @Test + fun purchase_for_existing_user() { + // given + val signal = CountDownLatch(1) + + val uid = "QON_test_uid1679992132407" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue(Maps.difference(expectedProducts, launchResult.products).areEqual()) + assertTrue(Maps.difference(expectedPermissions, launchResult.permissions).areEqual()) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + emptyMap(), + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + + val repository = initRepositoryForUid(uid) + + // when + repository.purchase(installDate, purchase, null, "test_monthly", callback) + + signal.await() + } + + @Test + fun restore() { + // given + val signal = CountDownLatch(1) + + val history = listOf( + History( + "google_monthly", + "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY", + 1679933171, + "SGD", + "6.99" + ) + ) + + val uid = uidPrefix + "_restore" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue(Maps.difference(expectedProducts, launchResult.products).areEqual()) + assertTrue(Maps.difference(expectedPermissions, launchResult.permissions).areEqual()) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + emptyMap(), + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.restoreRequest(installDate, history, callback) + } + + signal.await() + + // when + } + + @Test + fun attribution() { + // given + val signal = CountDownLatch(1) + val testAttributionInfo = mapOf( + "one" to 3, + "to be or not" to "be", + "toma" to "s" + ) + + val uid = uidPrefix + "_attribution" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.attribution( + testAttributionInfo, + QAttributionProvider.AppsFlyer.id, + { signal.countDown() }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun sendProperties() { + // given + val signal = CountDownLatch(1) + val testProperties = mapOf( + "customProperty" to "custom property value", + QUserProperty.CustomUserId.userPropertyCode to "custom user id" + ) + + val uid = uidPrefix + "_sendProperties" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.sendProperties( + testProperties, + { signal.countDown() }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun eligibilityForProductIds() { + // given + val signal = CountDownLatch(1) + val productIds = listOf(monthlyProduct.qonversionID, annualProduct.qonversionID) + val expectedResult = mapOf( + monthlyProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroProduct), + annualProduct.qonversionID to QEligibility(QIntroEligibilityStatus.Unknown), + inappProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroProduct) + ) + + val callback = object : QonversionEligibilityCallback { + override fun onSuccess(eligibilities: Map) { + assertTrue(Maps.difference(expectedResult, eligibilities).areEqual()) + + signal.countDown() + } + + override fun onError(error: QonversionError) { + fail("Shouldn't fail") + } + } + + val uid = uidPrefix + "_eligibilityForProductIds" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.eligibilityForProductIds( + productIds, + installDate, + callback + ) + } + + signal.await() + } + + @Test + fun identify() { + // given + val signal = CountDownLatch(1) + val uid = uidPrefix + "_identify" + val identityId = "identity_for_$uid" + + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.identify( + identityId, + uid, + { newAnonId -> + assertEquals(newAnonId, uid) + + signal.countDown() + }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun sendPushToken() { + // given + val signal = CountDownLatch(1) + + val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" + + val uid = uidPrefix + "_sendPushToken" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.sendPushToken(token) + } + + // then + // check that nothing critical happens + Handler(Looper.getMainLooper()).postDelayed( + { signal.countDown() }, + 1000 + ) + signal.await() + } + + @Test + fun screens() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_screens" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.screens( + noCodeScreenId, + { screen -> + assertEquals(screen.id, noCodeScreenId) + assertEquals(screen.background, "#CDFFD7") + assertEquals(screen.lang, "EN") + assertEquals(screen.obj, "screen") + assertTrue(screen.htmlPage.isNotEmpty()) + + signal.countDown() + }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun views() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_views" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.views(noCodeScreenId) + } + + // then + // check that nothing critical happens + Handler(Looper.getMainLooper()).postDelayed( + { signal.countDown() }, + 1000 + ) + signal.await() + } + + @Test + fun actionPoints() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_actionPoints" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.actionPoints( + mapOf( + "type" to "screen_view", + "active" to "1" + ), + { actionPoints -> + // no trigger for automation + assertTrue(actionPoints === null) + + signal.countDown() + }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + private fun withNewUserCreated( + repository: QonversionRepository, + onComplete: (error: QonversionError?) -> Unit + ) { + val data = InitRequestData( + installDate, + null, + emptyList(), + object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + onComplete(null) + } + + override fun onError(error: QonversionError, httpCode: Int?) { + onComplete(error) + } + } + ) + repository.init(data) + } + + private fun initRepositoryForUid(uid: String): QonversionRepository { + val qonversionConfig = QonversionConfig.Builder( + ApplicationProvider.getApplicationContext(), + "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a", + QLaunchMode.SubscriptionManagement + ).build() + val internalConfig = InternalConfig(qonversionConfig) + internalConfig.uid = uid + QDependencyInjector.buildAppComponent( + qonversionConfig.application, + internalConfig, + appStateProvider + ) + + return QDependencyInjector.appComponent.repository() + } +} diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOffering.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOffering.kt index 3b41e68d4..eecda9f92 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOffering.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOffering.kt @@ -3,6 +3,7 @@ package com.qonversion.android.sdk.dto.offerings import com.qonversion.android.sdk.internal.OfferingsDelegate import com.qonversion.android.sdk.dto.experiments.QExperimentInfo import com.qonversion.android.sdk.dto.products.QProduct +import com.qonversion.android.sdk.internal.equalsIgnoreOrder import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -25,4 +26,20 @@ class QOffering( fun productForID(id: String): QProduct? { return products.firstOrNull { it.qonversionID == id } } + + override fun hashCode(): Int { + return offeringID.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is QOffering && + other.offeringID == offeringID && + other.tag == tag && + other.products equalsIgnoreOrder products && + other.experimentInfo == experimentInfo + } + + override fun toString(): String { + return "QOffering(offeringID=$offeringID, tag=$tag, products=$products, experimentInfo=$experimentInfo)" + } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOfferings.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOfferings.kt index 66f6d07b7..4bedbfc89 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOfferings.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/offerings/QOfferings.kt @@ -1,5 +1,7 @@ package com.qonversion.android.sdk.dto.offerings +import com.qonversion.android.sdk.internal.equalsIgnoreOrder + data class QOfferings( val main: QOffering?, val availableOfferings: List = listOf() @@ -7,4 +9,15 @@ data class QOfferings( fun offeringForID(id: String): QOffering? { return availableOfferings.firstOrNull { it.offeringID == id } } + + override fun hashCode(): Int { + return super.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is QOfferings && + (other === this || + main == other.main && + availableOfferings equalsIgnoreOrder other.availableOfferings) + } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionRepository.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionRepository.kt index 07cc6a49a..37d8ed40f 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionRepository.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionRepository.kt @@ -1,6 +1,7 @@ package com.qonversion.android.sdk.internal import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback import com.qonversion.android.sdk.dto.QonversionError import com.qonversion.android.sdk.listeners.QonversionLaunchCallback @@ -88,18 +89,31 @@ internal class QonversionRepository internal constructor( historyRecords: List, callback: QonversionLaunchCallback? ) { - restoreRequest(installDate, historyRecords, callback) + val history = convertHistory(historyRecords) + + restoreRequest(installDate, history, callback) } - fun attribution(conversionInfo: Map, from: String) { + fun attribution( + conversionInfo: Map, + from: String, + onSuccess: (() -> Unit)? = null, + onError: ((error: QonversionError) -> Unit)? = null + ) { val attributionRequest = createAttributionRequest(conversionInfo, from) api.attribution(attributionRequest).enqueue { onResponse = { logger.release("AttributionRequest - ${it.getLogMessage()}") + if (it.isSuccessful) { + onSuccess?.invoke() + } else { + onError?.invoke(errorMapper.getErrorFromResponse(it)) + } } onFailure = { logger.release("AttributionRequest - failure - ${it.toQonversionError()}") + onError?.invoke(it.toQonversionError()) } } } @@ -473,12 +487,12 @@ internal class QonversionRepository internal constructor( } } - private fun restoreRequest( + @VisibleForTesting + internal fun restoreRequest( installDate: Long, - historyRecords: List, + history: List, callback: QonversionLaunchCallback? ) { - val history = convertHistory(historyRecords) val request = RestoreRequest( installDate = installDate, device = environmentProvider.getInfo(advertisingId), diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/extensions.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/extensions.kt index 69509c6ac..b4934a09d 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/extensions.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/extensions.kt @@ -97,3 +97,6 @@ internal fun Map.toEntitlementsMap(): Map res[id] = QEntitlement(permission) } return res } + +internal infix fun List.equalsIgnoreOrder(other: List) = + this.size == other.size && this.toSet() == other.toSet() From cfd3bbb7fe5f8df7133983caf4364ebdbab14c6e Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Thu, 6 Apr 2023 13:07:00 +0300 Subject: [PATCH 2/7] Integration tests for QonversionRepository directed at the outager. (#471) * Integration tests for QonversionRepository directed at the outager. * Detekt fixes * Update OutagerIntegrationTest.kt * Detekt fixes --------- Co-authored-by: GitHub Action --- config/detekt/baseline.xml | 10 + .../sdk/internal/OutagerIntegrationTest.kt | 568 ++++++++++++++++++ .../QonversionRepositoryIntegrationTest.kt | 2 +- 3 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 9ec7ee13e..749b9a4f0 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -135,6 +135,11 @@ MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"subscription_started\", \"happened\": $timeInSec}" to AutomationsEventType.SubscriptionStarted MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"subscription_upgraded\", \"happened\": $timeInSec}" to AutomationsEventType.SubscriptionUpgraded MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"trial_billing_retry_entered\", \"happened\": $timeInSec}" to AutomationsEventType.TrialBillingRetry + MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" + MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$detailsToken = "AEuhp4Kd9cZ3ZlkS2MylEXHBcZVLjwwllncPBm4a6lrVvj3uYGICnsE5w87i81qNsa38DPOW08BcZfLxJFxIWeISVwoBkT55tA2Bb6cKGsip724=" + MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" + MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" + MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest.<no name provided>$assertEquals(error.additionalMessage, """HTTP status code=503, data={"message":"Service Unavailable","code":0,"status":503}. """) MaxLineLength:PurchasesCacheTest.kt$PurchasesCacheTest$"\"purchaseToken\":\"gfegjilekkmecbonpfjiaakm.AO-J1OxQCaAn0NPlHTh5CoOiXK0p19X7qEymW9SHtssrggp7S9YafjA1oPBPlWO4Ur3W5rtyNJBzIrVoLOb5In0Jxofv4xV_7t1HaUYYd_f8xOBk7nRIY7g\"," MaxLineLength:PurchasesCacheTest.kt$PurchasesCacheTest$private val fourPurchasesStr = "[${generatePurchaseJson()},${generatePurchaseJson("2")},${generatePurchaseJson("3")},${generatePurchaseJson("4")}]" MaxLineLength:PurchasesCacheTest.kt$PurchasesCacheTest$purchaseToken = "gfegjilekkmecbonpfjiaakm.AO-J1OxQCaAn0NPlHTh5CoOiXK0p19X7qEymW9SHtssrggp7S9YafjA1oPBPlWO4Ur3W5rtyNJBzIrVoLOb5In0Jxofv4xV_7t1HaUYYd_f8xOBk7nRIY7g" @@ -190,6 +195,11 @@ MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:135 MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:142 MaximumLineLength:com.qonversion.android.sdk.automations.mvp.ScreenPresenterTest.kt:159 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:111 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:230 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:360 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:419 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:87 MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:157 MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:159 MaximumLineLength:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:172 diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt new file mode 100644 index 000000000..83fb5753f --- /dev/null +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt @@ -0,0 +1,568 @@ +package com.qonversion.android.sdk.internal + +import android.os.Handler +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.collect.Maps +import com.qonversion.android.sdk.QonversionConfig +import com.qonversion.android.sdk.dto.QAttributionProvider +import com.qonversion.android.sdk.dto.QEntitlementSource +import com.qonversion.android.sdk.dto.QLaunchMode +import com.qonversion.android.sdk.dto.QUserProperty +import com.qonversion.android.sdk.dto.QonversionError +import com.qonversion.android.sdk.dto.QonversionErrorCode +import com.qonversion.android.sdk.dto.eligibility.QEligibility +import com.qonversion.android.sdk.dto.offerings.QOffering +import com.qonversion.android.sdk.dto.offerings.QOfferingTag +import com.qonversion.android.sdk.dto.offerings.QOfferings +import com.qonversion.android.sdk.dto.products.QProduct +import com.qonversion.android.sdk.dto.products.QProductDuration +import com.qonversion.android.sdk.dto.products.QProductType +import com.qonversion.android.sdk.internal.di.QDependencyInjector +import com.qonversion.android.sdk.internal.dto.QLaunchResult +import com.qonversion.android.sdk.internal.dto.QPermission +import com.qonversion.android.sdk.internal.dto.QProductRenewState +import com.qonversion.android.sdk.internal.dto.purchase.History +import com.qonversion.android.sdk.internal.dto.request.data.InitRequestData +import com.qonversion.android.sdk.internal.provider.AppStateProvider +import com.qonversion.android.sdk.internal.purchase.Purchase +import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback +import com.qonversion.android.sdk.listeners.QonversionLaunchCallback +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date +import java.util.concurrent.CountDownLatch + +private val uidPrefix = "QON_test_uid_outager_android_" + System.currentTimeMillis() + +@RunWith(AndroidJUnit4::class) +internal class OutagerIntegrationTest { + + private val appStateProvider = object : AppStateProvider { + override val appState: AppState + get() = AppState.Foreground + } + + private val installDate = 1679652674L + + private val noCodeScreenId = "lsarjYcU" + + private val monthlyProduct = QProduct( + "test_monthly", + "google_monthly", + QProductType.Subscription, + QProductDuration.Monthly + ) + private val annualProduct = + QProduct("test_annual", "google_annual", QProductType.Trial, QProductDuration.Annual) + private val inappProduct = QProduct("test_inapp", "google_inapp", QProductType.InApp, null) + private val expectedProducts = mapOf( + monthlyProduct.qonversionID to monthlyProduct, + annualProduct.qonversionID to annualProduct, + inappProduct.qonversionID to inappProduct + ) + + private val expectedOffering = QOffering( + "main", + QOfferingTag.Main, + listOf(annualProduct, monthlyProduct) + ) + + private val expectedOfferings = QOfferings( + expectedOffering, + listOf(expectedOffering) + ) + + private val expectedProductPermissions = mapOf( + "test_monthly" to listOf("premium"), + "test_annual" to listOf("premium"), + "test_inapp" to listOf("noAds") + ) + + private val purchase = Purchase( + detailsToken = "AEuhp4Kd9cZ3ZlkS2MylEXHBcZVLjwwllncPBm4a6lrVvj3uYGICnsE5w87i81qNsa38DPOW08BcZfLxJFxIWeISVwoBkT55tA2Bb6cKGsip724=", + title = "DONT CHANGE! Sub for integration tests. (Qonversion Sample)", + description = "", + productId = "google_monthly", + type = "subs", + originalPrice = "$6.99", + originalPriceAmountMicros = 6990000, + priceCurrencyCode = "SGD", + price = "6.99", + priceAmountMicros = 6990000, + periodUnit = 2, + periodUnitsCount = 1, + freeTrialPeriod = "", + introductoryAvailable = false, + introductoryPriceAmountMicros = 0, + introductoryPrice = "0.00", + introductoryPriceCycles = 0, + introductoryPeriodUnit = 0, + introductoryPeriodUnitsCount = null, + orderId = "GPA.3307-0767-0668-99058", + originalOrderId = "GPA.3307-0767-0668-99058", + packageName = "com.qonversion.sample", + purchaseTime = 1679933171, + purchaseState = 1, + purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY", + acknowledged = false, + autoRenewing = true, + paymentMode = 0 + ) + + @Test + fun init() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_init" + val data = InitRequestData( + installDate, + null, + emptyList(), + object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue( + Maps.difference(expectedProducts, launchResult.products).areEqual() + ) + assertTrue( + Maps.difference(emptyMap(), launchResult.permissions).areEqual() + ) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + expectedProductPermissions, + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + ) + + val repository = initRepositoryForUid(uid) + + // when + repository.init(data) + + signal.await() + } + + @Test + fun purchase() { + // given + val signal = CountDownLatch(1) + + val expectedPermissions = mapOf( + "premium" to QPermission( + "premium", + "test_monthly", + QProductRenewState.Unknown, + Date(1679933171000), + Date(1682525171000), // plus month + QEntitlementSource.Unknown, + 1 + ) + ) + + val uid = uidPrefix + "_purchase" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue( + Maps.difference(expectedProducts, launchResult.products).areEqual() + ) + assertTrue(Maps.difference(expectedPermissions, launchResult.permissions).areEqual()) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + expectedProductPermissions, + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.purchase( + installDate, + purchase, + null, + "test_monthly", + callback + ) + } + + signal.await() + } + + @Test + fun restore() { + // given + val signal = CountDownLatch(1) + + val history = listOf( + History( + "google_monthly", + "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY", + 1679933171, + "SGD", + "6.99" + ) + ) + val expectedPermissions = mapOf( + "premium" to QPermission( + "premium", + "test_monthly", + QProductRenewState.Unknown, + Date(1679933171000), + Date(1680537971000), // plus seven days + QEntitlementSource.Unknown, + 1 + ) + ) + + val uid = uidPrefix + "_restore" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + // then + assertEquals(launchResult.uid, uid) + assertTrue( + Maps.difference(expectedProducts, launchResult.products).areEqual() + ) + assertTrue( + Maps.difference(expectedPermissions, launchResult.permissions).areEqual() + ) + assertEquals(expectedOfferings, launchResult.offerings) + assertTrue( + Maps.difference( + expectedProductPermissions, + launchResult.productPermissions!! + ).areEqual() + ) + signal.countDown() + } + + override fun onError(error: QonversionError, httpCode: Int?) { + fail("Shouldn't fail") + } + } + + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.restoreRequest(installDate, history, callback) + } + + signal.await() + } + + @Test + fun attribution() { + // given + val signal = CountDownLatch(1) + val testAttributionInfo = mapOf( + "one" to 3, + "to be or not" to "be", + "toma" to "s" + ) + + val uid = uidPrefix + "_attribution" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.attribution( + testAttributionInfo, + QAttributionProvider.AppsFlyer.id, + { signal.countDown() }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun sendProperties() { + // given + val signal = CountDownLatch(1) + val testProperties = mapOf( + "customProperty" to "custom property value", + QUserProperty.CustomUserId.userPropertyCode to "custom user id" + ) + + val uid = uidPrefix + "_sendProperties" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.sendProperties( + testProperties, + { signal.countDown() }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun eligibilityForProductIds() { + // given + val signal = CountDownLatch(1) + val productIds = listOf(monthlyProduct.qonversionID, annualProduct.qonversionID) + + val callback = object : QonversionEligibilityCallback { + override fun onSuccess(eligibilities: Map) { + fail("Shouldn't succeed") + } + + override fun onError(error: QonversionError) { + // Unsupported method on Outager + assertEquals(error.code, QonversionErrorCode.BackendError) + assertEquals(error.additionalMessage, """HTTP status code=503, data={"message":"Service Unavailable","code":0,"status":503}. """) + signal.countDown() + } + } + + val uid = uidPrefix + "_eligibilityForProductIds" + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.eligibilityForProductIds( + productIds, + installDate, + callback + ) + } + + signal.await() + } + + @Test + fun identify() { + // given + val signal = CountDownLatch(1) + val uid = uidPrefix + "_identify" + val identityId = "identity_for_$uid" + + val repository = initRepositoryForUid(uid) + + // when and then + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.identify( + identityId, + uid, + { newAnonId -> + assertEquals(newAnonId, uid) + + signal.countDown() + }, + { fail("Shouldn't fail") } + ) + } + + signal.await() + } + + @Test + fun sendPushToken() { + // given + val signal = CountDownLatch(1) + + val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" + + val uid = uidPrefix + "_sendPushToken" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.sendPushToken(token) + } + + // then + // check that nothing critical happens + Handler(Looper.getMainLooper()).postDelayed( + { signal.countDown() }, + 1000 + ) + signal.await() + } + + @Test + fun screens() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_screens" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { initError -> + initError?.let { + fail("Failed to create user") + } + + repository.screens( + noCodeScreenId, + { fail("Shouldn't succeed") }, + { error -> + // Unsupported method on Outager + assertEquals(error.code, QonversionErrorCode.BackendError) + assertEquals(error.additionalMessage, """HTTP status code=503, error=Service Unavailable. """) + signal.countDown() + } + ) + } + + signal.await() + } + + @Test + fun views() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_views" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { error -> + error?.let { + fail("Failed to create user") + } + + repository.views(noCodeScreenId) + } + + // then + // check that nothing critical happens + Handler(Looper.getMainLooper()).postDelayed( + { signal.countDown() }, + 1000 + ) + signal.await() + } + + @Test + fun actionPoints() { + // given + val signal = CountDownLatch(1) + + val uid = uidPrefix + "_actionPoints" + val repository = initRepositoryForUid(uid) + + // when + withNewUserCreated(repository) { initError -> + initError?.let { + fail("Failed to create user") + } + + repository.actionPoints( + mapOf( + "type" to "screen_view", + "active" to "1" + ), + { fail("Shouldn't succeed") }, + { error -> + // Unsupported method on Outager + assertEquals(error.code, QonversionErrorCode.BackendError) + assertEquals(error.additionalMessage, """HTTP status code=503, error=Service Unavailable. """) + signal.countDown() + } + ) + } + + signal.await() + } + + private fun withNewUserCreated( + repository: QonversionRepository, + onComplete: (error: QonversionError?) -> Unit + ) { + val data = InitRequestData( + installDate, + null, + emptyList(), + object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + onComplete(null) + } + + override fun onError(error: QonversionError, httpCode: Int?) { + onComplete(error) + } + } + ) + repository.init(data) + } + + private fun initRepositoryForUid(uid: String): QonversionRepository { + val qonversionConfig = QonversionConfig.Builder( + ApplicationProvider.getApplicationContext(), + "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a", + QLaunchMode.SubscriptionManagement + ) + .setProxyURL("") + .build() + val internalConfig = InternalConfig(qonversionConfig) + internalConfig.uid = uid + QDependencyInjector.buildAppComponent( + qonversionConfig.application, + internalConfig, + appStateProvider + ) + + return QDependencyInjector.appComponent.repository() + } +} diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt index 33d32ba3d..88324ef84 100644 --- a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt @@ -29,7 +29,7 @@ import com.qonversion.android.sdk.internal.provider.AppStateProvider import com.qonversion.android.sdk.internal.purchase.Purchase import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback import com.qonversion.android.sdk.listeners.QonversionLaunchCallback -import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test From f9bc6c10ff33b05c841303f7c834255a369d769b Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Thu, 6 Apr 2023 20:17:02 +0300 Subject: [PATCH 3/7] Planned integration testing workflow. (#473) * Integration tests for QonversionRepository directed at the outager. * Detekt fixes * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Planned integration testing workflow. * Simplify tests file path * Simplify tests file path * Simplify tests file path --------- Co-authored-by: GitHub Action --- .github/workflows/integration_tests.yml | 28 +++++++++++++++++++++++++ fastlane/Fastfile | 13 ++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/workflows/integration_tests.yml diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 000000000..8d4e2d73c --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,28 @@ +name: Planned integration tests + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * *' + +jobs: + testing: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Set Outager url + run: | + fastlane setOutagerUrl url:${{ secrets.OUTAGER_URL }} + + - name: Build and Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 28 + script: ./gradlew sdk:connectedAndroidTest \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a3d3baac4..d020369a4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,6 +23,13 @@ def update_gradle(new_version) update_file(path, regex, result_value) end +def update_outager_url_in_tests(url) + path = Dir['../**/OutagerIntegrationTest.kt'].first + regex = /\/ + + update_file(path, regex, url) +end + def update_file(path, regex, result_value) file = File.read(path) new_content = file.gsub(regex, result_value) @@ -97,4 +104,10 @@ platform :android do update_gradle(new_version) end + lane :setOutagerUrl do |options| + path = options[:url] + + update_outager_url_in_tests(path) + end + end \ No newline at end of file From 5f5c50f705d296b1126f443fb4835b40eebb91f4 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Mon, 17 Apr 2023 11:23:59 +0300 Subject: [PATCH 4/7] API integration tests for error cases. (#474) * API integration tests for error cases. * Detekt fixes. * Test fixes. * Detekt fixes. --------- Co-authored-by: GitHub Action --- config/detekt/baseline.xml | 21 +- .../sdk/internal/OutagerIntegrationTest.kt | 51 +-- .../QonversionRepositoryIntegrationTest.kt | 342 ++++++++++++++++-- 3 files changed, 354 insertions(+), 60 deletions(-) diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 749b9a4f0..87aeca055 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -167,6 +167,7 @@ MaxLineLength:QonversionConfig.kt$QonversionConfig.Builder$* MaxLineLength:QonversionError.kt$QonversionErrorCode$* MaxLineLength:QonversionInternal.kt$QonversionInternal$val isHistoricalDataSynced: Boolean = sharedPreferencesCache?.getBool(Constants.IS_HISTORICAL_DATA_SYNCED) ?: false + MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"""HTTP status code=400, data={"message":"Invalid access token received","code":10003,"status":400,"extra":[]}. """ MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$detailsToken = "AEuhp4Kd9cZ3ZlkS2MylEXHBcZVLjwwllncPBm4a6lrVvj3uYGICnsE5w87i81qNsa38DPOW08BcZfLxJFxIWeISVwoBkT55tA2Bb6cKGsip724=" MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" @@ -195,19 +196,21 @@ MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:135 MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:142 MaximumLineLength:com.qonversion.android.sdk.automations.mvp.ScreenPresenterTest.kt:159 - MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:111 - MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:230 - MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:360 - MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:419 - MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:87 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:112 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:231 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:361 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:420 + MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:88 MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:157 MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:159 MaximumLineLength:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:172 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionInternal.kt:114 - MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:123 - MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:259 - MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:439 - MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:99 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:103 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:127 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:325 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:376 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:646 + MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:867 MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:117 MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:118 MaximumLineLength:com.qonversion.android.sdk.internal.billing.QonversionBillingService.kt:109 diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt index 83fb5753f..0cd72f9c8 100644 --- a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt @@ -37,7 +37,8 @@ import org.junit.runner.RunWith import java.util.Date import java.util.concurrent.CountDownLatch -private val uidPrefix = "QON_test_uid_outager_android_" + System.currentTimeMillis() +private const val PROJECT_KEY = "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a" +private val UID_PREFIX = "QON_test_uid_outager_android_" + System.currentTimeMillis() @RunWith(AndroidJUnit4::class) internal class OutagerIntegrationTest { @@ -119,7 +120,7 @@ internal class OutagerIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_init" + val uid = UID_PREFIX + "_init" val data = InitRequestData( installDate, null, @@ -150,7 +151,7 @@ internal class OutagerIntegrationTest { } ) - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when repository.init(data) @@ -175,7 +176,7 @@ internal class OutagerIntegrationTest { ) ) - val uid = uidPrefix + "_purchase" + val uid = UID_PREFIX + "_purchase" val callback = object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { // then @@ -199,7 +200,7 @@ internal class OutagerIntegrationTest { } } - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -245,7 +246,7 @@ internal class OutagerIntegrationTest { ) ) - val uid = uidPrefix + "_restore" + val uid = UID_PREFIX + "_restore" val callback = object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { // then @@ -271,7 +272,7 @@ internal class OutagerIntegrationTest { } } - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -295,8 +296,8 @@ internal class OutagerIntegrationTest { "toma" to "s" ) - val uid = uidPrefix + "_attribution" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_attribution" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -324,8 +325,8 @@ internal class OutagerIntegrationTest { QUserProperty.CustomUserId.userPropertyCode to "custom user id" ) - val uid = uidPrefix + "_sendProperties" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_sendProperties" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -362,8 +363,8 @@ internal class OutagerIntegrationTest { } } - val uid = uidPrefix + "_eligibilityForProductIds" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_eligibilityForProductIds" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -385,10 +386,10 @@ internal class OutagerIntegrationTest { fun identify() { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_identify" + val uid = UID_PREFIX + "_identify" val identityId = "identity_for_$uid" - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -418,8 +419,8 @@ internal class OutagerIntegrationTest { val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" - val uid = uidPrefix + "_sendPushToken" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_sendPushToken" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -444,8 +445,8 @@ internal class OutagerIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_screens" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_screens" + val repository = initRepository(uid) // when withNewUserCreated(repository) { initError -> @@ -473,8 +474,8 @@ internal class OutagerIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_views" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_views" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -499,8 +500,8 @@ internal class OutagerIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_actionPoints" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_actionPoints" + val repository = initRepository(uid) // when withNewUserCreated(repository) { initError -> @@ -547,10 +548,10 @@ internal class OutagerIntegrationTest { repository.init(data) } - private fun initRepositoryForUid(uid: String): QonversionRepository { + private fun initRepository(uid: String, projectKey: String = PROJECT_KEY): QonversionRepository { val qonversionConfig = QonversionConfig.Builder( ApplicationProvider.getApplicationContext(), - "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a", + projectKey, QLaunchMode.SubscriptionManagement ) .setProxyURL("") diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt index 88324ef84..b6adea72e 100644 --- a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt @@ -11,6 +11,7 @@ import com.qonversion.android.sdk.dto.QEntitlementSource import com.qonversion.android.sdk.dto.QLaunchMode import com.qonversion.android.sdk.dto.QUserProperty import com.qonversion.android.sdk.dto.QonversionError +import com.qonversion.android.sdk.dto.QonversionErrorCode import com.qonversion.android.sdk.dto.eligibility.QEligibility import com.qonversion.android.sdk.dto.eligibility.QIntroEligibilityStatus import com.qonversion.android.sdk.dto.offerings.QOffering @@ -30,6 +31,7 @@ import com.qonversion.android.sdk.internal.purchase.Purchase import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback import com.qonversion.android.sdk.listeners.QonversionLaunchCallback import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test @@ -37,7 +39,9 @@ import org.junit.runner.RunWith import java.util.Date import java.util.concurrent.CountDownLatch -private val uidPrefix = "QON_test_uid_android_" + System.currentTimeMillis() +private const val PROJECT_KEY = "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a" +private const val INCORRECT_PROJECT_KEY = "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-aaaaa" +private val UID_PREFIX = "QON_test_uid_android_" + System.currentTimeMillis() @RunWith(AndroidJUnit4::class) internal class QonversionRepositoryIntegrationTest { @@ -131,7 +135,7 @@ internal class QonversionRepositoryIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_init" + val uid = UID_PREFIX + "_init" val data = InitRequestData( installDate, null, @@ -158,7 +162,38 @@ internal class QonversionRepositoryIntegrationTest { } ) - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) + + // when + repository.init(data) + + signal.await() + } + + @Test + fun initError() { + // given + val signal = CountDownLatch(1) + + val uid = UID_PREFIX + "_init" + val data = InitRequestData( + installDate, + null, + emptyList(), + object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + fail("Shouldn't succeed") + } + + override fun onError(error: QonversionError, httpCode: Int?) { + // then + assertIncorrectProjectKeyError(error) + signal.countDown() + } + } + ) + + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) // when repository.init(data) @@ -171,7 +206,7 @@ internal class QonversionRepositoryIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_purchase" + val uid = UID_PREFIX + "_purchase" val callback = object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { // then @@ -193,7 +228,7 @@ internal class QonversionRepositoryIntegrationTest { } } - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -240,7 +275,7 @@ internal class QonversionRepositoryIntegrationTest { } } - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when repository.purchase(installDate, purchase, null, "test_monthly", callback) @@ -248,6 +283,37 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun purchaseError() { + // given + val signal = CountDownLatch(1) + + val uid = UID_PREFIX + "_purchase" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + fail("Shouldn't succeed") + } + + override fun onError(error: QonversionError, httpCode: Int?) { + assertIncorrectProjectKeyError(error) + signal.countDown() + } + } + + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when + repository.purchase( + installDate, + purchase, + null, + "test_monthly", + callback + ) + + signal.await() + } + @Test fun restore() { // given @@ -263,7 +329,7 @@ internal class QonversionRepositoryIntegrationTest { ) ) - val uid = uidPrefix + "_restore" + val uid = UID_PREFIX + "_restore" val callback = object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { // then @@ -285,7 +351,7 @@ internal class QonversionRepositoryIntegrationTest { } } - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -297,8 +363,41 @@ internal class QonversionRepositoryIntegrationTest { } signal.await() + } + + @Test + fun restoreError() { + // given + val signal = CountDownLatch(1) + + val history = listOf( + History( + "google_monthly", + "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY", + 1679933171, + "SGD", + "6.99" + ) + ) + + val uid = UID_PREFIX + "_restore" + val callback = object : QonversionLaunchCallback { + override fun onSuccess(launchResult: QLaunchResult) { + fail("Shouldn't succeed") + } + + override fun onError(error: QonversionError, httpCode: Int?) { + assertIncorrectProjectKeyError(error) + signal.countDown() + } + } + + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) // when + repository.restoreRequest(installDate, history, callback) + + signal.await() } @Test @@ -311,8 +410,8 @@ internal class QonversionRepositoryIntegrationTest { "toma" to "s" ) - val uid = uidPrefix + "_attribution" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_attribution" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -331,6 +430,33 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun attributionError() { + // given + val signal = CountDownLatch(1) + val testAttributionInfo = mapOf( + "one" to 3, + "to be or not" to "be", + "toma" to "s" + ) + + val uid = UID_PREFIX + "_attribution" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when and then + repository.attribution( + testAttributionInfo, + QAttributionProvider.AppsFlyer.id, + { fail("Shouldn't succeed") }, + { error -> + assertAccessDeniedError(error) + signal.countDown() + } + ) + + signal.await() + } + @Test fun sendProperties() { // given @@ -340,8 +466,8 @@ internal class QonversionRepositoryIntegrationTest { QUserProperty.CustomUserId.userPropertyCode to "custom user id" ) - val uid = uidPrefix + "_sendProperties" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_sendProperties" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -359,6 +485,32 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun sendPropertiesError() { + // given + val signal = CountDownLatch(1) + val testProperties = mapOf( + "customProperty" to "custom property value", + QUserProperty.CustomUserId.userPropertyCode to "custom user id" + ) + + val uid = UID_PREFIX + "_sendProperties" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when and then + repository.sendProperties( + testProperties, + { fail("Shouldn't fail") }, + { error -> + assertNotNull(error) + assertIncorrectProjectKeyError(error!!) + signal.countDown() + } + ) + + signal.await() + } + @Test fun eligibilityForProductIds() { // given @@ -382,8 +534,8 @@ internal class QonversionRepositoryIntegrationTest { } } - val uid = uidPrefix + "_eligibilityForProductIds" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_eligibilityForProductIds" + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -401,14 +553,45 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun eligibilityForProductIdsError() { + // given + val signal = CountDownLatch(1) + val productIds = listOf(monthlyProduct.qonversionID, annualProduct.qonversionID) + + val callback = object : QonversionEligibilityCallback { + override fun onSuccess(eligibilities: Map) { + fail("Shouldn't succeed") + } + + override fun onError(error: QonversionError) { + assertIncorrectProjectKeyError(error) + + signal.countDown() + } + } + + val uid = UID_PREFIX + "_eligibilityForProductIds" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when and then + repository.eligibilityForProductIds( + productIds, + installDate, + callback + ) + + signal.await() + } + @Test fun identify() { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_identify" + val uid = UID_PREFIX + "_identify" val identityId = "identity_for_$uid" - val repository = initRepositoryForUid(uid) + val repository = initRepository(uid) // when and then withNewUserCreated(repository) { error -> @@ -431,6 +614,30 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun identifyError() { + // given + val signal = CountDownLatch(1) + val uid = UID_PREFIX + "_identify" + val identityId = "identity_for_$uid" + + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when and then + repository.identify( + identityId, + uid, + { fail("Shouldn't succeed") }, + { error -> + assertAccessDeniedError(error) + + signal.countDown() + } + ) + + signal.await() + } + @Test fun sendPushToken() { // given @@ -438,8 +645,8 @@ internal class QonversionRepositoryIntegrationTest { val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" - val uid = uidPrefix + "_sendPushToken" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_sendPushToken" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -464,8 +671,8 @@ internal class QonversionRepositoryIntegrationTest { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_screens" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_screens" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -491,13 +698,35 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun screensError() { + // given + val signal = CountDownLatch(1) + + val uid = UID_PREFIX + "_screens" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when + repository.screens( + noCodeScreenId, + { fail("Shouldn't succeed") }, + { error -> + assertAccessDeniedError(error) + + signal.countDown() + } + ) + + signal.await() + } + @Test fun views() { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_views" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_views" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -517,13 +746,33 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun viewsError() { + // given + val signal = CountDownLatch(1) + + val uid = UID_PREFIX + "_views" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when + repository.views(noCodeScreenId) + + // then + // check that nothing critical happens + Handler(Looper.getMainLooper()).postDelayed( + { signal.countDown() }, + 1000 + ) + signal.await() + } + @Test fun actionPoints() { // given val signal = CountDownLatch(1) - val uid = uidPrefix + "_actionPoints" - val repository = initRepositoryForUid(uid) + val uid = UID_PREFIX + "_actionPoints" + val repository = initRepository(uid) // when withNewUserCreated(repository) { error -> @@ -549,6 +798,31 @@ internal class QonversionRepositoryIntegrationTest { signal.await() } + @Test + fun actionPointsError() { + // given + val signal = CountDownLatch(1) + + val uid = UID_PREFIX + "_actionPoints" + val repository = initRepository(uid, INCORRECT_PROJECT_KEY) + + // when + repository.actionPoints( + mapOf( + "type" to "screen_view", + "active" to "1" + ), + { fail("Shouldn't succeed") }, + { error -> + assertAccessDeniedError(error) + + signal.countDown() + } + ) + + signal.await() + } + private fun withNewUserCreated( repository: QonversionRepository, onComplete: (error: QonversionError?) -> Unit @@ -570,10 +844,10 @@ internal class QonversionRepositoryIntegrationTest { repository.init(data) } - private fun initRepositoryForUid(uid: String): QonversionRepository { + private fun initRepository(uid: String, projectKey: String = PROJECT_KEY): QonversionRepository { val qonversionConfig = QonversionConfig.Builder( ApplicationProvider.getApplicationContext(), - "V4pK6FQo3PiDPj_2vYO1qZpNBbFXNP-a", + projectKey, QLaunchMode.SubscriptionManagement ).build() val internalConfig = InternalConfig(qonversionConfig) @@ -586,4 +860,20 @@ internal class QonversionRepositoryIntegrationTest { return QDependencyInjector.appComponent.repository() } + + private fun assertIncorrectProjectKeyError(error: QonversionError) { + assertEquals(error.code, QonversionErrorCode.InvalidCredentials) + assertTrue(listOf( + """HTTP status code=400, data={"message":"Invalid access token received","code":10003,"status":400,"extra":[]}. """, + """HTTP status code=401, data={"code":10003,"message":"Invalid access token received"}. """ + ).contains(error.additionalMessage)) + } + + private fun assertAccessDeniedError(error: QonversionError) { + assertEquals(error.code, QonversionErrorCode.BackendError) + assertTrue(listOf( + "HTTP status code=400, . ", + "HTTP status code=401, error=User with specified access token does not exist. " + ).contains(error.additionalMessage)) + } } From 62f5187cdd5adada4a1021c09c5d3a1c951c8823 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Fri, 19 May 2023 15:09:33 +0300 Subject: [PATCH 5/7] Fixing parallel requests and race conditions for entitlements state changing calls (#475) * Fixing parallel requests and race conditions for entitlements state changing calls. * Detekt fixes * Clear permissions cache on identify only if anon id changes --------- Co-authored-by: GitHub Action --- config/detekt/baseline.xml | 1 + .../com/qonversion/android/sdk/Qonversion.kt | 5 +- .../sdk/internal/QProductCenterManager.kt | 165 +++++++++++------- .../storage/LaunchResultCacheWrapper.kt | 5 + 4 files changed, 112 insertions(+), 64 deletions(-) diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 87aeca055..13df0bf66 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -367,6 +367,7 @@ TooManyFunctions:Api.kt$Api TooManyFunctions:AppComponent.kt$AppComponent TooManyFunctions:Cache.kt$Cache + TooManyFunctions:LaunchResultCacheWrapper.kt$LaunchResultCacheWrapper TooManyFunctions:QAutomationsManager.kt$QAutomationsManager TooManyFunctions:QProductCenterManager.kt$QProductCenterManager : PurchasesListenerOfferingsDelegate TooManyFunctions:Qonversion.kt$Qonversion diff --git a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt index 8854514ee..bbd0876d1 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt @@ -65,7 +65,8 @@ interface Qonversion { } /** - Call this function to sync the subscriber data with the first launch when Qonversion is implemented. + * Call this function to sync the subscriber data with the first launch + * when Qonversion is implemented. */ fun syncHistoricalData() @@ -199,7 +200,7 @@ interface Qonversion { fun restore(callback: QonversionEntitlementsCallback) /** - * This method will send all purchases to the Qonversion backend. Call this every time when purchase is handled by you own implementation. + * This method will send all purchases to the Qonversion backend. Call this every time when purchase is handled by your own implementation. * @warning This function should only be called if you're using Qonversion SDK in analytics mode. * @see [Analytics mode](https://qonversion.io/docs/observer-mode) */ diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt index b3f9abe82..8776a25bf 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt @@ -8,6 +8,7 @@ import android.util.Pair import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.SkuDetails +import com.qonversion.android.sdk.dto.QEntitlement import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback import com.qonversion.android.sdk.dto.QonversionError import com.qonversion.android.sdk.dto.QonversionErrorCode @@ -64,6 +65,8 @@ internal class QProductCenterManager internal constructor( private val isLaunchingFinished: Boolean get() = launchError != null || launchResultCache.sessionLaunchResult != null + private var isRestoreInProgress = false + private var loadProductsState = NotStartedYet private var skuDetails = mapOf() @@ -73,6 +76,7 @@ internal class QProductCenterManager internal constructor( private var productsCallbacks = mutableListOf() private var entitlementCallbacks = mutableListOf() private var purchasingCallbacks = mutableMapOf() + private var restoreCallbacks = mutableListOf() private var processingPartnersIdentityId: String? = null private var pendingPartnersIdentityId: String? = null @@ -130,6 +134,8 @@ internal class QProductCenterManager internal constructor( callback: QonversionLaunchCallback? = null ) { val launchCallback: QonversionLaunchCallback = getLaunchCallback(callback) + launchError = null + launchResultCache.resetSessionCache() if (!internalConfig.primaryConfig.isKidsMode) { val adProvider = AdvertisingProvider() @@ -176,11 +182,10 @@ internal class QProductCenterManager internal constructor( return } - launchResultCache.clearPermissionsCache() unhandledLogoutAvailable = false pendingPartnersIdentityId = userID - if (!isLaunchingFinished) { + if (!isLaunchingFinished || isRestoreInProgress) { return } @@ -215,10 +220,11 @@ internal class QProductCenterManager internal constructor( processingPartnersIdentityId = null if (currentUserID == identityID) { - executeEntitlementsBlock() + handleEntitlementRequestsAfterIdentityChanges() } else { internalConfig.uid = identityID + launchResultCache.clearPermissionsCache() launch() } } @@ -450,46 +456,44 @@ internal class QProductCenterManager internal constructor( fun checkEntitlements(callback: QonversionEntitlementsCallback) { entitlementCallbacks.add(callback) - if (!isLaunchingFinished || processingPartnersIdentityId != null) { - return - } - - val pendingIdentityID = pendingPartnersIdentityId - if (!pendingIdentityID.isNullOrEmpty()) { - identify(pendingIdentityID) - return - } - - executeEntitlementsBlock() + handleEntitlementRequestsAfterIdentityChanges() } fun restore(callback: QonversionEntitlementsCallback? = null) { - billingService.queryPurchasesHistory(onQueryHistoryCompleted = { historyRecords -> - consumer.consumeHistoryRecords(historyRecords) - val skuIds = historyRecords.mapNotNull { it.historyRecord.sku } - val loadedSkuDetails = - skuDetails.filter { skuIds.contains(it.value.sku) }.toMutableMap() - val resultSkuIds = (skuIds - loadedSkuDetails.keys).toSet() - - if (resultSkuIds.isNotEmpty()) { - billingService.loadProducts(resultSkuIds, onLoadCompleted = { - it.forEach { singleSkuDetails -> - run { - loadedSkuDetails[singleSkuDetails.sku] = singleSkuDetails - skuDetails = skuDetails + (singleSkuDetails.sku to singleSkuDetails) + callback?.let { restoreCallbacks.add(it) } + + if (isRestoreInProgress) { + return + } + isRestoreInProgress = true + + billingService.queryPurchasesHistory( + onQueryHistoryCompleted = { historyRecords -> + consumer.consumeHistoryRecords(historyRecords) + val skuIds = historyRecords.mapNotNull { it.historyRecord.sku } + val loadedSkuDetails = + skuDetails.filter { skuIds.contains(it.value.sku) }.toMutableMap() + val resultSkuIds = (skuIds - loadedSkuDetails.keys).toSet() + + if (resultSkuIds.isNotEmpty()) { + billingService.loadProducts(resultSkuIds, onLoadCompleted = { + it.forEach { singleSkuDetails -> + run { + loadedSkuDetails[singleSkuDetails.sku] = singleSkuDetails + skuDetails = skuDetails + (singleSkuDetails.sku to singleSkuDetails) + } } - } - processRestore(historyRecords, loadedSkuDetails, callback) - }, onLoadFailed = { - processRestore(historyRecords, loadedSkuDetails, callback) - }) - } else { - processRestore(historyRecords, loadedSkuDetails, callback) - } - }, + processRestore(historyRecords, loadedSkuDetails) + }, onLoadFailed = { + processRestore(historyRecords, loadedSkuDetails) + }) + } else { + processRestore(historyRecords, loadedSkuDetails) + } + }, onQueryHistoryFailed = { - callback?.onError(it.toQonversionError()) + executeRestoreBlocksOnError(it.toQonversionError()) }) } @@ -520,8 +524,7 @@ internal class QProductCenterManager internal constructor( private fun processRestore( purchaseHistoryRecords: List, - loadedSkuDetails: Map, - callback: QonversionEntitlementsCallback? = null + loadedSkuDetails: Map ) { purchaseHistoryRecords.forEach { purchaseHistory -> val skuDetails = loadedSkuDetails[purchaseHistory.historyRecord.sku] @@ -534,14 +537,14 @@ internal class QProductCenterManager internal constructor( object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) { updateLaunchResult(launchResult) - callback?.onSuccess(launchResult.permissions.toEntitlementsMap()) + executeRestoreBlocksOnSuccess(launchResult.permissions.toEntitlementsMap()) } override fun onError(error: QonversionError, httpCode: Int?) { if (shouldCalculatePermissionsLocally(error, httpCode)) { - calculateRestorePermissionsLocally(purchaseHistoryRecords, callback, error) + calculateRestorePermissionsLocally(purchaseHistoryRecords, error) } else { - callback?.onError(error) + executeRestoreBlocksOnError(error) } } }) @@ -549,12 +552,10 @@ internal class QProductCenterManager internal constructor( private fun calculateRestorePermissionsLocally( purchaseHistoryRecords: List, - callback: QonversionEntitlementsCallback?, restoreError: QonversionError ) { val launchResult = launchResultCache.getLaunchResult() ?: run { - failLocallyGrantingPermissionsWithError( - callback, + failLocallyGrantingRestorePermissionsWithError( launchError ?: QonversionError(QonversionErrorCode.LaunchError) ) return @@ -571,8 +572,8 @@ internal class QProductCenterManager internal constructor( it ) - callback?.onSuccess(permissions.toEntitlementsMap()) - } ?: failLocallyGrantingPermissionsWithError(callback, restoreError) + executeRestoreBlocksOnSuccess(permissions.toEntitlementsMap()) + } ?: failLocallyGrantingRestorePermissionsWithError(restoreError) } private fun calculatePurchasePermissionsLocally( @@ -581,7 +582,7 @@ internal class QProductCenterManager internal constructor( purchaseError: QonversionError ) { val launchResult = launchResultCache.getLaunchResult() ?: run { - failLocallyGrantingPermissionsWithError( + failLocallyGrantingPurchasePermissionsWithError( purchaseCallback, launchError ?: QonversionError(QonversionErrorCode.LaunchError) ) @@ -596,7 +597,7 @@ internal class QProductCenterManager internal constructor( val purchasedProduct = launchResult.products.values.find { product -> product.skuDetail?.sku == purchase.sku } ?: run { - failLocallyGrantingPermissionsWithError(purchaseCallback, purchaseError) + failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) return } @@ -606,10 +607,10 @@ internal class QProductCenterManager internal constructor( it ) purchaseCallback?.onSuccess(permissions.toEntitlementsMap()) - } ?: failLocallyGrantingPermissionsWithError(purchaseCallback, purchaseError) + } ?: failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) } - private fun failLocallyGrantingPermissionsWithError( + private fun failLocallyGrantingPurchasePermissionsWithError( callback: QonversionEntitlementsCallback?, error: QonversionError ) { @@ -617,6 +618,13 @@ internal class QProductCenterManager internal constructor( callback?.onError(error) } + private fun failLocallyGrantingRestorePermissionsWithError( + error: QonversionError + ) { + launchResultCache.clearPermissionsCache() + executeRestoreBlocksOnError(error) + } + private fun grantPermissionsAfterFailedPurchaseTracking( purchase: Purchase, purchasedProduct: QProduct, @@ -795,16 +803,7 @@ internal class QProductCenterManager internal constructor( launchError = null - if (processingPartnersIdentityId == null) { - val pendingIdentityId = pendingPartnersIdentityId - if (!pendingIdentityId.isNullOrEmpty()) { - identify(pendingIdentityId) - } else if (unhandledLogoutAvailable) { - handleLogout() - } else { - executeEntitlementsBlock() - } - } + handleEntitlementRequestsAfterIdentityChanges() loadStoreProductsIfPossible() @@ -816,8 +815,9 @@ internal class QProductCenterManager internal constructor( override fun onError(error: QonversionError, httpCode: Int?) { launchError = error + handleEntitlementRequestsAfterIdentityChanges(error) + loadStoreProductsIfPossible() - executeEntitlementsBlock(error.takeIf { pendingPartnersIdentityId != null }) callback?.onError(error, httpCode) } @@ -906,6 +906,25 @@ internal class QProductCenterManager internal constructor( } } + /** + * Executes identity changing operations (identify or logout) if pending requests exist. + * Else executes awaiting entitlements requests. + */ + private fun handleEntitlementRequestsAfterIdentityChanges(lastError: QonversionError? = null) { + if (!isLaunchingFinished || isRestoreInProgress || processingPartnersIdentityId != null) { + return + } + + val pendingIdentityId = pendingPartnersIdentityId + if (!pendingIdentityId.isNullOrEmpty()) { + identify(pendingIdentityId) + } else if (unhandledLogoutAvailable) { + handleLogout() + } else { + executeEntitlementsBlock(lastError) + } + } + private fun handleCachedPurchases() { val cachedPurchases = purchasesCache.loadPurchases() cachedPurchases.forEach { purchase -> @@ -973,6 +992,28 @@ internal class QProductCenterManager internal constructor( } } + private fun executeRestoreBlocksOnSuccess(entitlements: Map) { + val callbacks = restoreCallbacks.toList() + restoreCallbacks.clear() + + isRestoreInProgress = false + + callbacks.forEach { callback -> callback.onSuccess(entitlements) } + + handleEntitlementRequestsAfterIdentityChanges() + } + + private fun executeRestoreBlocksOnError(error: QonversionError) { + val callbacks = restoreCallbacks.toList() + restoreCallbacks.clear() + + isRestoreInProgress = false + + callbacks.forEach { callback -> callback.onError(error) } + + handleEntitlementRequestsAfterIdentityChanges(error) + } + private fun retryLaunchForProducts(onCompleted: () -> Unit) { launchResultCache.sessionLaunchResult?.let { handleLoadStateForProducts(onCompleted) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt index 3dc3b9e61..251a03a19 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt @@ -46,6 +46,11 @@ internal class LaunchResultCacheWrapper( } } + fun resetSessionCache() { + sessionLaunchResult = null + permissions = null + } + fun clearPermissionsCache() { permissions = null cache.remove(PERMISSIONS_KEY) From 5d80054c2d86a73e582915fe7016d1924f6c2fde Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Fri, 19 May 2023 15:24:59 +0300 Subject: [PATCH 6/7] Simplify `handleEntitlementRequestsAfterIdentityChanges` method name. (#476) * Fixing parallel requests and race conditions for entitlements state changing calls. * Detekt fixes * Clear permissions cache on identify only if anon id changes * Rename method --------- Co-authored-by: GitHub Action --- .../android/sdk/internal/QProductCenterManager.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt index 8776a25bf..0adbaf792 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt @@ -220,7 +220,7 @@ internal class QProductCenterManager internal constructor( processingPartnersIdentityId = null if (currentUserID == identityID) { - handleEntitlementRequestsAfterIdentityChanges() + handlePendingRequests() } else { internalConfig.uid = identityID @@ -456,7 +456,7 @@ internal class QProductCenterManager internal constructor( fun checkEntitlements(callback: QonversionEntitlementsCallback) { entitlementCallbacks.add(callback) - handleEntitlementRequestsAfterIdentityChanges() + handlePendingRequests() } fun restore(callback: QonversionEntitlementsCallback? = null) { @@ -803,7 +803,7 @@ internal class QProductCenterManager internal constructor( launchError = null - handleEntitlementRequestsAfterIdentityChanges() + handlePendingRequests() loadStoreProductsIfPossible() @@ -815,7 +815,7 @@ internal class QProductCenterManager internal constructor( override fun onError(error: QonversionError, httpCode: Int?) { launchError = error - handleEntitlementRequestsAfterIdentityChanges(error) + handlePendingRequests(error) loadStoreProductsIfPossible() @@ -910,7 +910,7 @@ internal class QProductCenterManager internal constructor( * Executes identity changing operations (identify or logout) if pending requests exist. * Else executes awaiting entitlements requests. */ - private fun handleEntitlementRequestsAfterIdentityChanges(lastError: QonversionError? = null) { + private fun handlePendingRequests(lastError: QonversionError? = null) { if (!isLaunchingFinished || isRestoreInProgress || processingPartnersIdentityId != null) { return } @@ -1000,7 +1000,7 @@ internal class QProductCenterManager internal constructor( callbacks.forEach { callback -> callback.onSuccess(entitlements) } - handleEntitlementRequestsAfterIdentityChanges() + handlePendingRequests() } private fun executeRestoreBlocksOnError(error: QonversionError) { @@ -1011,7 +1011,7 @@ internal class QProductCenterManager internal constructor( callbacks.forEach { callback -> callback.onError(error) } - handleEntitlementRequestsAfterIdentityChanges(error) + handlePendingRequests(error) } private fun retryLaunchForProducts(onCompleted: () -> Unit) { From 8539a897d984aa2f1c3b8fd97c31c7f645cd862d Mon Sep 17 00:00:00 2001 From: SpertsyanKM Date: Fri, 19 May 2023 12:26:38 +0000 Subject: [PATCH 7/7] [create-pull-request] automated change --- build.gradle | 2 +- fastlane/README.md | 8 ++++++++ fastlane/report.xml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index eae886b4f..6db93291a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask buildscript { ext { release = [ - versionName: "4.4.0", + versionName: "4.4.1", versionCode: 1 ] } diff --git a/fastlane/README.md b/fastlane/README.md index 2372b8b89..79d52dc8c 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -47,6 +47,14 @@ Runs all the tests +### android setOutagerUrl + +```sh +[bundle exec] fastlane android setOutagerUrl +``` + + + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/fastlane/report.xml b/fastlane/report.xml index 3cd25efb4..995f7a700 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,7 +5,7 @@ - +