Add AppUpdateManagerTest

This commit is contained in:
Torsten Grote committed 2025-05-22 13:41:52 -03:00
1 parent 8ec1d39f1f
commit a69915ee89
6 files changed
+310 -7

No files matched your search

+2
View File
@@ -226,8 +226,10 @@ dependencies {
testImplementation libs.androidx.test.core
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.mockk
testImplementation libs.mockito.core
testImplementation libs.hamcrest
testImplementation libs.slf4j.simple
androidTestImplementation libs.androidx.test.core
androidTestImplementation libs.androidx.core.testing
+5 -1
View File
@@ -19,7 +19,11 @@
-keep class junit.** { *; }
-dontwarn junit.**
-keep class kotlin.reflect.** { *; }
-keep class io.mockk.** { *; }
-keep class kotlin.io.** { *; }
-keep class kotlin.collections.** { *; }
-keep class java.util.concurrent.Executor { *; }
-keep class androidx.arch.core.executor.ArchTaskExecutor {*;}
@@ -28,4 +32,4 @@
}
# This is necessary so that RemoteWorkManager can be initialized (also marked with @Keep)
-keep class androidx.work.WorkManager { *; }
-keep class androidx.work.** { *; }
@@ -2,29 +2,31 @@ package org.fdroid.fdroid
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import mu.KotlinLogging
import org.acra.util.versionCodeLong
import org.fdroid.database.DbUpdateChecker
import org.fdroid.database.Repository
import org.fdroid.database.UpdatableApp
import org.fdroid.download.DownloaderFactory
import org.fdroid.fdroid.AppUpdateStatusManager.Status.Downloading
import org.fdroid.fdroid.data.Apk
import org.fdroid.fdroid.data.App
import org.fdroid.fdroid.installer.ApkCache
import org.fdroid.fdroid.installer.InstallManagerService
import org.fdroid.fdroid.installer.InstallerFactory
import org.fdroid.fdroid.net.DownloaderFactory
import org.fdroid.index.RepoManager
class AppUpdateManager(
class AppUpdateManager @JvmOverloads constructor(
private val context: Context,
private val repoManager: RepoManager,
private val updateChecker: DbUpdateChecker,
private val downloaderFactory: DownloaderFactory =
org.fdroid.fdroid.net.DownloaderFactory.INSTANCE,
private val statusManager: AppUpdateStatusManager = AppUpdateStatusManager.getInstance(context),
) {
private val log = KotlinLogging.logger { }
private val downloaderFactory = DownloaderFactory.INSTANCE
private val statusManager = AppUpdateStatusManager.getInstance(context)
fun updateApps() {
// get apps with updates pending
@@ -33,6 +35,7 @@ class AppUpdateManager(
.sortedWith { app1, app2 ->
// our own app will be last to update
if (app1.packageName == context.packageName) return@sortedWith 1
if (app2.packageName == context.packageName) return@sortedWith -1
// other apps are sorted by name
(app1.name ?: "").compareTo(app2.name ?: "", ignoreCase = true)
}
@@ -46,7 +49,7 @@ class AppUpdateManager(
private val installManagerService = InstallManagerService.getInstance(context)
private val legacyApp = App(app)
private val legacyApk = Apk(app.update, repo)
private val uri = Uri.parse(legacyApk.canonicalUrl)
private val uri = legacyApk.canonicalUrl.toUri()
private var lastProgress = 0L
override fun onInstallProcessStarted() {
@@ -80,7 +83,7 @@ class AppUpdateManager(
// legacy cruft
val legacyApp = App(app)
val legacyApk = Apk(app.update, repo)
val uri = Uri.parse(legacyApk.canonicalUrl)
val uri = legacyApk.canonicalUrl.toUri()
// check if app was already installed in the meantime
try {
val packageInfo = context.packageManager.getPackageInfo(app.packageName, 0)
@@ -0,0 +1,288 @@
package org.fdroid.fdroid
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import io.mockk.verifyOrder
import org.fdroid.database.AppManifest
import org.fdroid.database.AppVersion
import org.fdroid.database.DbUpdateChecker
import org.fdroid.database.Repository
import org.fdroid.database.UpdatableApp
import org.fdroid.download.Downloader
import org.fdroid.fdroid.data.App
import org.fdroid.fdroid.installer.InstallManagerService
import org.fdroid.fdroid.installer.Installer
import org.fdroid.fdroid.installer.InstallerFactory
import org.fdroid.index.RepoManager
import org.fdroid.index.v2.FileV1
import org.junit.Test
import java.io.File
import java.io.IOException
class AppUpdateManagerTest {
private val context: Context = mockk()
private val repoManager: RepoManager = mockk()
private val updateChecker: DbUpdateChecker = mockk()
private val preferences: Preferences = mockk()
private val downloaderFactory: org.fdroid.download.DownloaderFactory = mockk()
private val statusManager: AppUpdateStatusManager = mockk()
private val appUpdateManager =
AppUpdateManager(context, repoManager, updateChecker, downloaderFactory, statusManager)
private val downloader: Downloader = mockk()
private val installer: Installer = mockk()
private val packageManager: PackageManager = mockk()
private val installManagerService: InstallManagerService = mockk(relaxed = true)
private val repoUri: Uri = mockk(relaxed = true)
private val app1 = App().apply {
packageName = "org.example.1"
name = "One"
repoId = 42
}
private val app2 = App().apply {
packageName = "org.example.2"
name = "Two"
repoId = 42
}
private val repo: Repository = mockk(relaxed = true) {
every { repoId } returns app1.repoId
every { address } returns "https://example.org/repo"
}
private val file1 = FileV1("one", "one")
private val file2 = FileV1("two", "one")
private val version1: AppVersion = mockk<AppVersion>(relaxed = true) {
every { repoId } returns app1.repoId
every { packageName } returns app1.packageName
every { manifest } returns AppManifest("1", 1)
every { file } returns file1
}
private val version2: AppVersion = mockk<AppVersion>(relaxed = true) {
every { repoId } returns app2.repoId
every { packageName } returns app2.packageName
every { manifest } returns AppManifest("2", 2)
every { file } returns file2
}
// need to mock apps as their constructor is internal
private val updatableApp1: UpdatableApp = mockk(relaxed = true) {
every { packageName } returns app1.packageName
every { name } returns app1.name
every { summary } returns app1.summary
every { repoId } returns app1.repoId
every { update } returns version1
every { update.file } returns file1
every { update.manifest } returns AppManifest("1", 1)
every { update.repoId } returns app1.repoId
every { update.packageName } returns app1.packageName
every { installedVersionCode } returns app1.installedVersionCode
}
private val updatableApp2: UpdatableApp = mockk(relaxed = true) {
every { packageName } returns app2.packageName
every { name } returns app2.name
every { summary } returns app2.summary
every { repoId } returns app2.repoId
every { update } returns version2
every { update.repoId } returns app2.repoId
every { update.packageName } returns app2.packageName
every { update.file } returns file2
every { update.manifest } returns AppManifest("2", 2)
every { installedVersionCode } returns app2.installedVersionCode
}
init {
mockkStatic(Preferences::get)
every { Preferences.get() } returns preferences
mockkStatic(InstallManagerService::getInstance)
every { InstallManagerService.getInstance(any()) } returns installManagerService
mockkStatic(Uri::parse)
every { Uri.parse(any()) } returns repoUri
mockkStatic(InstallerFactory::create)
every { InstallerFactory.create(any(), any(), any()) } returns installer
every { context.packageManager } returns packageManager
}
@Test
fun testNoUpdates() {
val updates = emptyList<UpdatableApp>()
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { statusManager.addUpdatableApps(updates, false) } just Runs
appUpdateManager.updateApps()
}
@Test
fun testSomeUpdates() {
val updates = listOf(updatableApp1, updatableApp2)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloaderFactory.create(repo, any(), file2, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } just Runs
every { installer.installPackage(any(), any()) } just Runs
appUpdateManager.updateApps()
verify(exactly = 2) {
installManagerService.onDownloadComplete(any())
installer.installPackage(any(), any())
}
}
@Test
fun testVersionAlreadyInstalled() {
val updates = listOf(updatableApp1, updatableApp2)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(app1.packageName, any<Int>())
} returns getPackageInfo(1)
every {
packageManager.getPackageInfo(app2.packageName, any<Int>())
} returns getPackageInfo(2)
appUpdateManager.updateApps()
verify(exactly = 0) {
installManagerService.onDownloadComplete(any())
installer.installPackage(any(), any())
}
}
@Test
fun testOurOwnAppIsUpdatedLast() {
val installer1: Installer = mockk()
val installer2: Installer = mockk()
val updates = listOf(updatableApp1, updatableApp2)
val updatesSorted = listOf(updatableApp2, updatableApp1)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns app1.packageName // we are app1
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updatesSorted, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloaderFactory.create(repo, any(), file2, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } just Runs
every {
InstallerFactory.create(any(), match { it.packageName == app1.packageName }, any())
} returns installer1
every {
InstallerFactory.create(any(), match { it.packageName == app2.packageName }, any())
} returns installer2
every { installer1.installPackage(any(), any()) } just Runs
every { installer2.installPackage(any(), any()) } just Runs
appUpdateManager.updateApps()
verifyOrder {
// app1 gets installed last, because it is us and updating us kills us
installer2.installPackage(any(), any())
installer1.installPackage(any(), any())
}
}
@Test
fun testFailedDownloadSkipsUpdate() {
val updates = listOf(updatableApp1)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } throws IOException("foo bar")
appUpdateManager.updateApps()
verify(exactly = 0) {
installer.installPackage(any(), any())
}
verify {
installManagerService.onDownloadFailed(any(), any())
}
}
private fun getPackageInfo(versionCode: Int) = PackageInfo().apply {
@Suppress("DEPRECATION") // longVersionCode doesn't work
this.versionCode = versionCode
}
}