Updated on 2026-08-14
This commit is contained in:
parent
26bba893ff
commit
51e650c360
47 changed files with 1628 additions and 73 deletions
1
domain/app-update/.gitignore
vendored
Normal file
1
domain/app-update/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
19
domain/app-update/build.gradle.kts
Normal file
19
domain/app-update/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
// region Test
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.domain.appupdate.model
|
||||
|
||||
/**
|
||||
* Result of checking whether the application needs an update.
|
||||
*/
|
||||
enum class AppUpdateState {
|
||||
|
||||
/** Update is mandatory — a blocking screen with an "Update now" button must be shown. */
|
||||
ForceUpdate,
|
||||
|
||||
/**
|
||||
* Update is mandatory but impossible on this device (OS too old for the critical version) —
|
||||
* a permanently blocking "brick" screen must be shown.
|
||||
*/
|
||||
Brick,
|
||||
|
||||
/**
|
||||
* Update is mandatory but the device OS is too old for the min-supported version — a blocking
|
||||
* "update your OS" screen must be shown.
|
||||
*/
|
||||
OsTooOld,
|
||||
|
||||
/** Update is available but optional — a dismissible screen may be shown. */
|
||||
OptionalUpdate,
|
||||
|
||||
/** No update is required. */
|
||||
NoUpdate,
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.domain.appupdate.model
|
||||
|
||||
internal class AppVersion private constructor(
|
||||
private val major: Int,
|
||||
private val minor: Int,
|
||||
private val fix: Int,
|
||||
) : Comparable<AppVersion> {
|
||||
|
||||
override fun compareTo(other: AppVersion): Int {
|
||||
major.compareTo(other.major).let { if (it != 0) return it }
|
||||
minor.compareTo(other.minor).let { if (it != 0) return it }
|
||||
return fix.compareTo(other.fix)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELIMITER = "."
|
||||
private const val MAJOR = 0
|
||||
private const val MINOR = 1
|
||||
private const val FIX = 2
|
||||
|
||||
fun parseOrNull(value: String): AppVersion? {
|
||||
// Drop build-type/pre-release suffixes ("6.1-internal", "1.0.0-SNAPSHOT") before parsing.
|
||||
val parts = value.trim().substringBefore('-').substringBefore('+').split(DELIMITER)
|
||||
|
||||
val major = parts.getOrNull(MAJOR)?.toIntOrNull() ?: return null
|
||||
val minor = parts.getOrNull(MINOR).toVersionPartOrNull() ?: return null
|
||||
val fix = parts.getOrNull(FIX).toVersionPartOrNull() ?: return null
|
||||
|
||||
return AppVersion(major = major, minor = minor, fix = fix)
|
||||
}
|
||||
|
||||
private fun String?.toVersionPartOrNull(): Int? = when (this) {
|
||||
null -> 0
|
||||
else -> toIntOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.appupdate.model
|
||||
|
||||
/**
|
||||
* Backend-driven update policy. All fields are nullable; a null threshold skips its check.
|
||||
*
|
||||
* @property minSupportedVersion mandatory-update threshold (inclusive): `installedVersion <= minSupportedVersion`
|
||||
* @property minSupportedOSVersion OS threshold for the min-supported case (exclusive): `deviceOsVersion < it` -> OS too old
|
||||
* @property criticalVersion critical-update threshold (inclusive): `installedVersion <= criticalVersion`
|
||||
* @property criticalOSVersion OS threshold for the critical case (exclusive): `deviceOsVersion < it` -> brick
|
||||
* @property latestVersion optional-update threshold (exclusive): `installedVersion < latestVersion`
|
||||
*/
|
||||
data class AppVersionInfo(
|
||||
val minSupportedVersion: String?,
|
||||
val minSupportedOSVersion: String?,
|
||||
val criticalVersion: String?,
|
||||
val criticalOSVersion: String?,
|
||||
val latestVersion: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.appupdate.model
|
||||
|
||||
data class OptionalUpdateShown(
|
||||
val version: String,
|
||||
val shownAtMillis: Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.appupdate.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.appupdate.model.AppVersionInfo
|
||||
import com.tangem.domain.appupdate.model.OptionalUpdateShown
|
||||
|
||||
interface AppUpdateRepository {
|
||||
|
||||
/** Last cached version thresholds, or `null` if nothing has been fetched yet. No network. */
|
||||
suspend fun getCachedAppVersionInfo(): AppVersionInfo?
|
||||
|
||||
/** Wall-clock time (millis) of the last successful fetch, or `null` if nothing has been fetched yet. */
|
||||
suspend fun getCachedAppVersionTimestamp(): Long?
|
||||
|
||||
/** Fetches fresh thresholds and, on success, overwrites the cache (and its timestamp). */
|
||||
suspend fun refreshAppVersionInfo(): Either<Throwable, AppVersionInfo>
|
||||
|
||||
suspend fun getOptionalUpdateShown(): OptionalUpdateShown?
|
||||
|
||||
suspend fun setOptionalUpdateShown(shown: OptionalUpdateShown)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.domain.appupdate.usecase
|
||||
|
||||
import com.tangem.domain.appupdate.model.AppUpdateState
|
||||
import com.tangem.domain.appupdate.model.AppVersion
|
||||
import com.tangem.domain.appupdate.model.AppVersionInfo
|
||||
import com.tangem.domain.appupdate.model.OptionalUpdateShown
|
||||
import com.tangem.domain.appupdate.repository.AppUpdateRepository
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
class GetAppUpdateStateUseCase(
|
||||
private val repository: AppUpdateRepository,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
private val currentTimeMillis: () -> Long = System::currentTimeMillis,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Instant decision computed from the cached thresholds and the current app/OS version. No network.
|
||||
* Records the optional update as shown when it decides to show it (24h throttle). Never throws —
|
||||
* any failure resolves to [AppUpdateState.NoUpdate] so the initial navigation is never blocked.
|
||||
*/
|
||||
suspend fun getCached(): AppUpdateState = runSuspendCatching {
|
||||
resolve(freshCachedInfoOrNull(), recordOptionalShown = true)
|
||||
}.getOrElse { error ->
|
||||
TangemLogger.e("Unable to resolve cached app update state", error)
|
||||
AppUpdateState.NoUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches fresh thresholds (overwriting the cache) and re-evaluates. Falls back to the cache on a
|
||||
* network error. Does not record the optional update — used for background and on-screen refreshes.
|
||||
* Never throws — any failure resolves to [AppUpdateState.NoUpdate].
|
||||
*/
|
||||
suspend fun refresh(): AppUpdateState = runSuspendCatching {
|
||||
val info = repository.refreshAppVersionInfo().getOrNull() ?: freshCachedInfoOrNull()
|
||||
resolve(info, recordOptionalShown = false)
|
||||
}.getOrElse { error ->
|
||||
TangemLogger.e("Unable to resolve app update state", error)
|
||||
AppUpdateState.NoUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached thresholds, but only while they are still fresh. A cache older than [CACHE_TTL_MILLIS] is
|
||||
* ignored so a permanently unreachable backend can't keep the user blocked forever — a successful
|
||||
* fetch is required at least once per TTL window to keep a blocking threshold in effect.
|
||||
*/
|
||||
private suspend fun freshCachedInfoOrNull(): AppVersionInfo? {
|
||||
val cachedAt = repository.getCachedAppVersionTimestamp() ?: return null
|
||||
if (currentTimeMillis() - cachedAt > CACHE_TTL_MILLIS) return null
|
||||
return repository.getCachedAppVersionInfo()
|
||||
}
|
||||
|
||||
private suspend fun resolve(info: AppVersionInfo?, recordOptionalShown: Boolean): AppUpdateState {
|
||||
info ?: return AppUpdateState.NoUpdate
|
||||
|
||||
val appVersion = AppVersion.parseOrNull(appInfoProvider.appVersion) ?: return AppUpdateState.NoUpdate
|
||||
val deviceOsVersion = AppVersion.parseOrNull(appInfoProvider.osVersion)
|
||||
val latestVersion = info.latestVersion?.let(AppVersion::parseOrNull)
|
||||
|
||||
val criticalVersion = info.criticalVersion?.let(AppVersion::parseOrNull)
|
||||
if (criticalVersion != null && appVersion <= criticalVersion && isEscapable(latestVersion, criticalVersion)) {
|
||||
return blockingStateFor(info.criticalOSVersion, deviceOsVersion, AppUpdateState.Brick)
|
||||
}
|
||||
|
||||
val minSupportedVersion = info.minSupportedVersion?.let(AppVersion::parseOrNull)
|
||||
if (minSupportedVersion != null &&
|
||||
appVersion <= minSupportedVersion &&
|
||||
isEscapable(latestVersion, minSupportedVersion)
|
||||
) {
|
||||
return blockingStateFor(info.minSupportedOSVersion, deviceOsVersion, AppUpdateState.OsTooOld)
|
||||
}
|
||||
|
||||
if (info.latestVersion != null && latestVersion != null && appVersion < latestVersion) {
|
||||
return resolveOptionalUpdate(info.latestVersion, recordOptionalShown)
|
||||
}
|
||||
|
||||
return AppUpdateState.NoUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* A blocking threshold is honored only if the advertised latest version is strictly above it — i.e.
|
||||
* updating actually clears the block. A threshold no installable version can satisfy is a backend
|
||||
* misconfiguration and is ignored.
|
||||
*/
|
||||
private fun isEscapable(latestVersion: AppVersion?, threshold: AppVersion): Boolean =
|
||||
latestVersion != null && latestVersion > threshold
|
||||
|
||||
private suspend fun resolveOptionalUpdate(latestVersion: String, recordOptionalShown: Boolean): AppUpdateState {
|
||||
if (!recordOptionalShown) return AppUpdateState.OptionalUpdate
|
||||
|
||||
val shown = repository.getOptionalUpdateShown()
|
||||
val isThrottled = shown != null &&
|
||||
shown.version == latestVersion &&
|
||||
currentTimeMillis() - shown.shownAtMillis < OPTIONAL_UPDATE_INTERVAL_MILLIS
|
||||
|
||||
if (isThrottled) return AppUpdateState.NoUpdate
|
||||
|
||||
repository.setOptionalUpdateShown(
|
||||
OptionalUpdateShown(version = latestVersion, shownAtMillis = currentTimeMillis()),
|
||||
)
|
||||
return AppUpdateState.OptionalUpdate
|
||||
}
|
||||
|
||||
private fun blockingStateFor(
|
||||
requiredOsVersion: String?,
|
||||
deviceOsVersion: AppVersion?,
|
||||
osTooOldState: AppUpdateState,
|
||||
): AppUpdateState {
|
||||
val requiredOs = requiredOsVersion?.let(AppVersion::parseOrNull)
|
||||
val cannotUpdate = requiredOs != null && deviceOsVersion != null && deviceOsVersion < requiredOs
|
||||
return if (cannotUpdate) osTooOldState else AppUpdateState.ForceUpdate
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val OPTIONAL_UPDATE_INTERVAL_MILLIS = 24L * 60 * 60 * 1000
|
||||
const val CACHE_TTL_MILLIS = 24L * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package com.tangem.domain.appupdate
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.appupdate.model.AppUpdateState
|
||||
import com.tangem.domain.appupdate.model.AppVersionInfo
|
||||
import com.tangem.domain.appupdate.model.OptionalUpdateShown
|
||||
import com.tangem.domain.appupdate.repository.AppUpdateRepository
|
||||
import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class GetAppUpdateStateUseCaseTest {
|
||||
|
||||
private val repository = mockk<AppUpdateRepository>()
|
||||
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||
private val useCase = GetAppUpdateStateUseCase(repository, appInfoProvider, currentTimeMillis = { NOW })
|
||||
|
||||
private fun givenCached(appVersion: String = "5.0", osVersion: String = "14", info: AppVersionInfo?) {
|
||||
every { appInfoProvider.appVersion } returns appVersion
|
||||
every { appInfoProvider.osVersion } returns osVersion
|
||||
coEvery { repository.getCachedAppVersionInfo() } returns info
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW
|
||||
coEvery { repository.getOptionalUpdateShown() } returns null
|
||||
coEvery { repository.setOptionalUpdateShown(any()) } just Runs
|
||||
}
|
||||
|
||||
private fun givenRefresh(appVersion: String = "5.0", osVersion: String = "14", info: AppVersionInfo) {
|
||||
every { appInfoProvider.appVersion } returns appVersion
|
||||
every { appInfoProvider.osVersion } returns osVersion
|
||||
coEvery { repository.refreshAppVersionInfo() } returns info.right()
|
||||
coEvery { repository.getOptionalUpdateShown() } returns null
|
||||
coEvery { repository.setOptionalUpdateShown(any()) } just Runs
|
||||
}
|
||||
|
||||
private fun info(
|
||||
minSupportedVersion: String? = null,
|
||||
minSupportedOSVersion: String? = null,
|
||||
criticalVersion: String? = null,
|
||||
criticalOSVersion: String? = null,
|
||||
latestVersion: String? = null,
|
||||
) = AppVersionInfo(
|
||||
minSupportedVersion = minSupportedVersion,
|
||||
minSupportedOSVersion = minSupportedOSVersion,
|
||||
criticalVersion = criticalVersion,
|
||||
criticalOSVersion = criticalOSVersion,
|
||||
latestVersion = latestVersion,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN app at critical version and OS ok WHEN getCached THEN ForceUpdate`() = runTest {
|
||||
givenCached(
|
||||
appVersion = "5.0",
|
||||
osVersion = "14",
|
||||
info = info(criticalVersion = "5.0", criticalOSVersion = "10", latestVersion = "5.1"),
|
||||
)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app at critical version and OS too old WHEN getCached THEN Brick`() = runTest {
|
||||
givenCached(
|
||||
appVersion = "5.0",
|
||||
osVersion = "9",
|
||||
info = info(criticalVersion = "5.0", criticalOSVersion = "10", latestVersion = "5.1"),
|
||||
)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.Brick)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app at min supported and OS ok WHEN getCached THEN ForceUpdate`() = runTest {
|
||||
givenCached(
|
||||
appVersion = "5.0",
|
||||
osVersion = "14",
|
||||
info = info(minSupportedVersion = "5.0", minSupportedOSVersion = "10", latestVersion = "5.1"),
|
||||
)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app at min supported and OS too old WHEN getCached THEN OsTooOld`() = runTest {
|
||||
givenCached(
|
||||
appVersion = "5.0",
|
||||
osVersion = "9",
|
||||
info = info(minSupportedVersion = "5.0", minSupportedOSVersion = "10", latestVersion = "5.1"),
|
||||
)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OsTooOld)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN critical above latest WHEN getCached THEN not blocking and degraded to optional`() = runTest {
|
||||
givenCached(appVersion = "5.20", info = info(criticalVersion = "9.99", latestVersion = "5.41"))
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN min supported above latest WHEN getCached THEN not blocking and degraded to optional`() = runTest {
|
||||
givenCached(appVersion = "5.20", info = info(minSupportedVersion = "9.99", latestVersion = "5.41"))
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN blocking threshold but no latest WHEN getCached THEN ignored as NoUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(criticalVersion = "5.0", criticalOSVersion = "10"))
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app below latest and not shown before WHEN getCached THEN OptionalUpdate is recorded`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(latestVersion = "5.37"))
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
coVerify(exactly = 1) {
|
||||
repository.setOptionalUpdateShown(OptionalUpdateShown(version = "5.37", shownAtMillis = NOW))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app at latest WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.37", info = info(latestVersion = "5.37"))
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN optional shown for same version within 24h WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(latestVersion = "5.37"))
|
||||
coEvery { repository.getOptionalUpdateShown() } returns
|
||||
OptionalUpdateShown(version = "5.37", shownAtMillis = NOW - DAY_MILLIS + 1)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
coVerify(exactly = 0) { repository.setOptionalUpdateShown(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN optional shown for same version over 24h ago WHEN getCached THEN OptionalUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(latestVersion = "5.37"))
|
||||
coEvery { repository.getOptionalUpdateShown() } returns
|
||||
OptionalUpdateShown(version = "5.37", shownAtMillis = NOW - DAY_MILLIS - 1)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN optional shown for older version WHEN getCached THEN OptionalUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(latestVersion = "5.37"))
|
||||
coEvery { repository.getOptionalUpdateShown() } returns
|
||||
OptionalUpdateShown(version = "5.36", shownAtMillis = NOW)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN all thresholds null WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(info = info())
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN critical and latest both match WHEN getCached THEN critical wins`() = runTest {
|
||||
givenCached(
|
||||
appVersion = "3.0",
|
||||
osVersion = "14",
|
||||
info = info(criticalVersion = "3.0", minSupportedVersion = "3.0", latestVersion = "5.37"),
|
||||
)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(info = null)
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cache older than TTL WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(criticalVersion = "5.0", latestVersion = "5.1"))
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW - DAY_MILLIS - 1
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cache exactly at TTL WHEN getCached THEN still blocks`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(criticalVersion = "5.0", latestVersion = "5.1"))
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW - DAY_MILLIS
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cache without timestamp WHEN getCached THEN NoUpdate`() = runTest {
|
||||
givenCached(appVersion = "5.0", info = info(criticalVersion = "5.0", latestVersion = "5.1"))
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns null
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refresh fails and cache stale WHEN refresh THEN NoUpdate`() = runTest {
|
||||
every { appInfoProvider.appVersion } returns "5.0"
|
||||
every { appInfoProvider.osVersion } returns "14"
|
||||
coEvery { repository.refreshAppVersionInfo() } returns IllegalStateException("error").left()
|
||||
coEvery { repository.getCachedAppVersionInfo() } returns info(criticalVersion = "5.0", latestVersion = "5.1")
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW - DAY_MILLIS - 1
|
||||
|
||||
assertThat(useCase.refresh()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refresh returns blocking info WHEN refresh THEN ForceUpdate`() = runTest {
|
||||
givenRefresh(appVersion = "5.0", osVersion = "14", info = info(criticalVersion = "5.0", latestVersion = "5.1"))
|
||||
|
||||
assertThat(useCase.refresh()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refresh returns optional info WHEN refresh THEN OptionalUpdate without recording`() = runTest {
|
||||
givenRefresh(appVersion = "5.0", info = info(latestVersion = "5.37"))
|
||||
|
||||
assertThat(useCase.refresh()).isEqualTo(AppUpdateState.OptionalUpdate)
|
||||
coVerify(exactly = 0) { repository.setOptionalUpdateShown(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refresh fails WHEN refresh THEN falls back to cached thresholds`() = runTest {
|
||||
every { appInfoProvider.appVersion } returns "5.0"
|
||||
every { appInfoProvider.osVersion } returns "14"
|
||||
coEvery { repository.refreshAppVersionInfo() } returns IllegalStateException("error").left()
|
||||
coEvery { repository.getCachedAppVersionInfo() } returns info(criticalVersion = "5.0", latestVersion = "5.1")
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW
|
||||
|
||||
assertThat(useCase.refresh()).isEqualTo(AppUpdateState.ForceUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN repository throws WHEN getCached THEN NoUpdate`() = runTest {
|
||||
coEvery { repository.getCachedAppVersionTimestamp() } returns NOW
|
||||
coEvery { repository.getCachedAppVersionInfo() } throws IllegalStateException("boom")
|
||||
|
||||
assertThat(useCase.getCached()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN repository throws WHEN refresh THEN NoUpdate`() = runTest {
|
||||
coEvery { repository.refreshAppVersionInfo() } throws IllegalStateException("boom")
|
||||
|
||||
assertThat(useCase.refresh()).isEqualTo(AppUpdateState.NoUpdate)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NOW = 1_000_000_000_000L
|
||||
const val DAY_MILLIS = 24L * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.domain.appupdate.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class AppVersionTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN single segment WHEN parse THEN parsed`() {
|
||||
assertThat(AppVersion.parseOrNull("14")).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN major and minor WHEN parse THEN parsed`() {
|
||||
assertThat(AppVersion.parseOrNull("5.30")).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN major minor fix WHEN parse THEN parsed`() {
|
||||
assertThat(AppVersion.parseOrNull("5.40.1")).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty minor part WHEN parse THEN null`() {
|
||||
assertThat(AppVersion.parseOrNull("5..1")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-numeric part WHEN parse THEN null`() {
|
||||
assertThat(AppVersion.parseOrNull("5.x")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN blank WHEN parse THEN null`() {
|
||||
assertThat(AppVersion.parseOrNull("")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN trailing dot WHEN parse THEN null`() {
|
||||
assertThat(AppVersion.parseOrNull("5.")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN older version WHEN compare THEN less than newer`() {
|
||||
assertThat(AppVersion.parseOrNull("14")!! < AppVersion.parseOrNull("15.0")!!).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fix difference WHEN compare THEN ordered`() {
|
||||
assertThat(AppVersion.parseOrNull("5.40.1")!! > AppVersion.parseOrNull("5.40.0")!!).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing minor WHEN compare to explicit zero THEN equal`() {
|
||||
val implicit = AppVersion.parseOrNull("14")!!
|
||||
val explicit = AppVersion.parseOrNull("14.0")!!
|
||||
|
||||
assertThat(implicit.compareTo(explicit)).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN build-type suffix WHEN parse THEN parsed without suffix`() {
|
||||
assertThat(AppVersion.parseOrNull("6.1-internal")).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN snapshot fallback WHEN parse THEN parsed`() {
|
||||
assertThat(AppVersion.parseOrNull("1.0.0-SNAPSHOT")).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN suffixed version WHEN compare to clean THEN equal`() {
|
||||
val suffixed = AppVersion.parseOrNull("6.1-internal")!!
|
||||
val clean = AppVersion.parseOrNull("6.1")!!
|
||||
|
||||
assertThat(suffixed.compareTo(clean)).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue