Move libraries into their own folder

and remove sharedTest symlink hack. The shared tests are now a proper gradle module to avoid issues with using the same source files in different modules.
This commit is contained in:
Torsten Grote
2022-07-20 16:06:50 -03:00
committed by Michael Pöhn
parent a6bce15116
commit f6075848e7
181 changed files with 265 additions and 433 deletions

View File

@@ -0,0 +1,145 @@
package org.fdroid.database
import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.database.TestUtils.toMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.sort
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class AppDaoTest : AppTest() {
@Test
fun insertGetDeleteSingleApp() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1)
assertEquals(app1, appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort())
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
appDao.deleteAppMetadata(repoId, packageName)
assertEquals(0, appDao.countApps())
assertEquals(0, appDao.countLocalizedFiles())
assertEquals(0, appDao.countLocalizedFileLists())
}
@Test
fun testGetSameAppFromTwoRepos() {
// insert same app into three repos (repoId1 has highest weight)
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId1, packageName, app1, locales)
appDao.insert(repoId2, packageName, app2, locales)
appDao.insert(repoId3, packageName, app3, locales)
// ensure expected repo weights
val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail()
val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail()
val repoPrefs3 = repoDao.getRepositoryPreferences(repoId3) ?: fail()
assertTrue(repoPrefs2.weight < repoPrefs3.weight)
assertTrue(repoPrefs3.weight < repoPrefs1.weight)
// each app gets returned as stored from each repo
assertEquals(app1, appDao.getApp(repoId1, packageName)?.toMetadataV2()?.sort())
assertEquals(app2, appDao.getApp(repoId2, packageName)?.toMetadataV2()?.sort())
assertEquals(app3, appDao.getApp(repoId3, packageName)?.toMetadataV2()?.sort())
// if repo is not given, app from repo with highest weight is returned
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
}
@Test
fun testUpdateCompatibility() {
// insert two apps with one version each
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
// without versions, app isn't compatible
assertEquals(false, appDao.getApp(repoId, packageName)?.metadata?.isCompatible)
appDao.updateCompatibility(repoId)
assertEquals(false, appDao.getApp(repoId, packageName)?.metadata?.isCompatible)
// still incompatible with incompatible version
versionDao.insert(repoId, packageName, "1", getRandomPackageVersionV2(), false)
appDao.updateCompatibility(repoId)
assertEquals(false, appDao.getApp(repoId, packageName)?.metadata?.isCompatible)
// only with at least one compatible version, the app becomes compatible
versionDao.insert(repoId, packageName, "2", getRandomPackageVersionV2(), true)
appDao.updateCompatibility(repoId)
assertEquals(true, appDao.getApp(repoId, packageName)?.metadata?.isCompatible)
}
@Test
fun testAfterLocalesChanged() {
// insert app with German and French locales
val localesBefore = LocaleListCompat.forLanguageTags("de-DE")
val app = app1.copy(
name = mapOf("de-DE" to "de-DE", "fr-FR" to "fr-FR"),
summary = mapOf("de-DE" to "de-DE", "fr-FR" to "fr-FR"),
)
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app, localesBefore)
// device is set to German, so name and summary come out German
val appBefore = appDao.getApp(repoId, packageName)
assertEquals("de-DE", appBefore?.name)
assertEquals("de-DE", appBefore?.summary)
// device gets switched to French
val localesAfter = LocaleListCompat.forLanguageTags("fr-FR")
db.afterLocalesChanged(localesAfter)
// device is set to French now, so name and summary come out French
val appAfter = appDao.getApp(repoId, packageName)
assertEquals("fr-FR", appAfter?.name)
assertEquals("fr-FR", appAfter?.summary)
}
@Test
fun testGetNumberOfAppsInCategory() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
// app1 is in A and B
appDao.insert(repoId, packageName1, app1, locales)
assertEquals(1, appDao.getNumberOfAppsInCategory("A"))
assertEquals(1, appDao.getNumberOfAppsInCategory("B"))
assertEquals(0, appDao.getNumberOfAppsInCategory("C"))
// app2 is in A
appDao.insert(repoId, packageName2, app2, locales)
assertEquals(2, appDao.getNumberOfAppsInCategory("A"))
assertEquals(1, appDao.getNumberOfAppsInCategory("B"))
assertEquals(0, appDao.getNumberOfAppsInCategory("C"))
// app3 is in A and B
appDao.insert(repoId, packageName3, app3, locales)
assertEquals(3, appDao.getNumberOfAppsInCategory("A"))
assertEquals(2, appDao.getNumberOfAppsInCategory("B"))
assertEquals(0, appDao.getNumberOfAppsInCategory("C"))
}
@Test
fun testGetNumberOfAppsInRepository() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
assertEquals(0, appDao.getNumberOfAppsInRepository(repoId))
appDao.insert(repoId, packageName1, app1, locales)
assertEquals(1, appDao.getNumberOfAppsInRepository(repoId))
appDao.insert(repoId, packageName2, app2, locales)
assertEquals(2, appDao.getNumberOfAppsInRepository(repoId))
appDao.insert(repoId, packageName3, app3, locales)
assertEquals(3, appDao.getNumberOfAppsInRepository(repoId))
}
}

View File

@@ -0,0 +1,433 @@
package org.fdroid.database
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockk
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.AppListSortOrder.LAST_UPDATED
import org.fdroid.database.AppListSortOrder.NAME
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.MetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class AppListItemsTest : AppTest() {
private val pm: PackageManager = mockk()
private val appPairs = listOf(
Pair(packageName1, app1),
Pair(packageName2, app2),
Pair(packageName3, app3),
)
@Test
fun testSearchQuery() {
val app1 = app1.copy(name = mapOf("en-US" to "One"), summary = mapOf("en-US" to "Onearry"))
val app2 = app2.copy(name = mapOf("en-US" to "Two"), summary = mapOf("de" to "Zfassung"))
val app3 = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung"))
// insert three apps in a random order
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName3, app3, locales)
appDao.insert(repoId, packageName2, app2, locales)
// nothing is installed
every { pm.getInstalledPackages(0) } returns emptyList()
// get first app by search, sort order doesn't matter
appDao.getAppListItems(pm, "One", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
// get second app by search, sort order doesn't matter
appDao.getAppListItems(pm, "Two", NAME).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app2, apps[0])
}
// get second and third app by searching for summary
appDao.getAppListItems(pm, "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(2, apps.size)
// sort-order isn't fixes, yet
if (apps[0].packageName == packageName2) {
assertEquals(app2, apps[0])
assertEquals(app3, apps[1])
} else {
assertEquals(app3, apps[0])
assertEquals(app2, apps[1])
}
}
// empty search for unknown search term
appDao.getAppListItems(pm, "foo bar", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(0, apps.size)
}
}
@Test
fun testSearchQueryInCategory() {
val app1 = app1.copy(name = mapOf("en-US" to "One"), summary = mapOf("en-US" to "Onearry"))
val app2 = app2.copy(name = mapOf("en-US" to "Two"), summary = mapOf("de" to "Zfassung"))
val app3 = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung"))
// insert three apps in a random order
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName3, app3, locales)
appDao.insert(repoId, packageName2, app2, locales)
// nothing is installed
every { pm.getInstalledPackages(0) } returns emptyList()
// get first app by search, sort order doesn't matter
appDao.getAppListItems(pm, "A", "One", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
// get second app by search, sort order doesn't matter
appDao.getAppListItems(pm, "A", "Two", NAME).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app2, apps[0])
}
// get second and third app by searching for summary
appDao.getAppListItems(pm, "A", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(2, apps.size)
// sort-order isn't fixes, yet
if (apps[0].packageName == packageName2) {
assertEquals(app2, apps[0])
assertEquals(app3, apps[1])
} else {
assertEquals(app3, apps[0])
assertEquals(app2, apps[1])
}
}
// get third app by searching for summary in category B only
appDao.getAppListItems(pm, "B", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app3, apps[0])
}
// empty search for unknown category
appDao.getAppListItems(pm, "C", "Zfassung", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(0, apps.size)
}
// empty search for unknown search term
appDao.getAppListItems(pm, "A", "foo bar", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(0, apps.size)
}
}
@Test
fun testSortOrderByLastUpdated() {
// insert three apps in a random order
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName3, app3, locales)
appDao.insert(repoId, packageName2, app2, locales)
// nothing is installed
every { pm.getInstalledPackages(0) } returns emptyList()
// get apps sorted by last updated
appDao.getAppListItems(pm, "", LAST_UPDATED).getOrFail().let { apps ->
assertEquals(3, apps.size)
// we expect apps to be sorted by last updated descending
appPairs.sortedByDescending { (_, metadataV2) ->
metadataV2.lastUpdated
}.forEachIndexed { i, pair ->
assertEquals(pair.first, apps[i].packageName)
assertEquals(pair.second, apps[i])
}
}
}
@Test
fun testSortOrderByName() {
// insert three apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
appDao.insert(repoId, packageName3, app3, locales)
// nothing is installed
every { pm.getInstalledPackages(0) } returns emptyList()
// get apps sorted by name ascending
appDao.getAppListItems(pm, null, NAME).getOrFail().let { apps ->
assertEquals(3, apps.size)
// we expect apps to be sorted by last updated descending
appPairs.sortedBy { (_, metadataV2) ->
metadataV2.name.getBestLocale(locales)
}.forEachIndexed { i, pair ->
assertEquals(pair.first, apps[i].packageName)
assertEquals(pair.second, apps[i])
}
}
}
@Test
fun testPackageManagerInfo() {
// insert two apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
// one of the apps is installed
@Suppress("DEPRECATION")
val packageInfo2 = PackageInfo().apply {
packageName = packageName2
versionName = getRandomString()
versionCode = Random.nextInt(1, Int.MAX_VALUE)
}
every { pm.getInstalledPackages(0) } returns listOf(packageInfo2)
// get apps sorted by name and last update, test on both lists
listOf(
appDao.getAppListItems(pm, "", NAME).getOrFail(),
appDao.getAppListItems(pm, null, LAST_UPDATED).getOrFail(),
).forEach { apps ->
assertEquals(2, apps.size)
// the installed app should have app data
val installed = if (apps[0].packageName == packageName1) apps[1] else apps[0]
val other = if (apps[0].packageName == packageName1) apps[0] else apps[1]
assertEquals(packageInfo2.versionName, installed.installedVersionName)
assertEquals(packageInfo2.getVersionCode(), installed.installedVersionCode)
assertNull(other.installedVersionName)
assertNull(other.installedVersionCode)
}
}
@Test
fun testCompatibility() {
// insert two apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
// both apps are not compatible
getItems { apps ->
assertEquals(2, apps.size)
assertFalse(apps[0].isCompatible)
assertFalse(apps[1].isCompatible)
}
// each app gets a version
versionDao.insert(repoId, packageName1, "1", getRandomPackageVersionV2(), true)
versionDao.insert(repoId, packageName2, "1", getRandomPackageVersionV2(), false)
// updating compatibility for apps
appDao.updateCompatibility(repoId)
// now only one is not compatible
getItems { apps ->
assertEquals(2, apps.size)
if (apps[0].packageName == packageName1) {
assertTrue(apps[0].isCompatible)
assertFalse(apps[1].isCompatible)
} else {
assertFalse(apps[0].isCompatible)
assertTrue(apps[1].isCompatible)
}
}
}
@Test
fun testAntiFeaturesFromHighestVersion() {
// insert one app with no versions
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
// app has no anti-features, because no version
getItems { apps ->
assertEquals(1, apps.size)
assertNull(apps[0].antiFeatures)
assertEquals(emptyList(), apps[0].antiFeatureKeys)
}
// app gets a version
val version1 = getRandomPackageVersionV2(42)
versionDao.insert(repoId, packageName1, "1", version1, true)
// app has now has the anti-features of the version
// note that installed versions don't contain anti-features, so they are ignored
getItems(alsoInstalled = false) { apps ->
assertEquals(1, apps.size)
assertEquals(version1.antiFeatures.map { it.key }, apps[0].antiFeatureKeys)
}
// app gets another version
val version2 = getRandomPackageVersionV2(23)
versionDao.insert(repoId, packageName1, "2", version2, true)
// app has now has the anti-features of the initial version still, because 2nd is lower
// note that installed versions don't contain anti-features, so they are ignored
getItems(alsoInstalled = false) { apps ->
assertEquals(1, apps.size)
assertEquals(version1.antiFeatures.map { it.key }, apps[0].antiFeatureKeys)
}
}
@Test
fun testOnlyFromEnabledRepos() {
// insert two apps in two different repos
val repoId = repoDao.insertOrReplace(getRandomRepo())
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId2, packageName2, app2, locales)
// initially both apps get returned
getItems { apps ->
assertEquals(2, apps.size)
}
// disable first repo
repoDao.setRepositoryEnabled(repoId, false)
// now only app from enabled repo gets returned
getItems { apps ->
assertEquals(1, apps.size)
assertEquals(repoId2, apps[0].repoId)
}
}
@Test
fun testFromRepoWithHighestWeight() {
// insert same app into three repos (repoId1 has highest weight)
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName, app2, locales)
appDao.insert(repoId1, packageName, app1, locales)
appDao.insert(repoId3, packageName, app3, locales)
// ensure expected repo weights
val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail()
val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail()
val repoPrefs3 = repoDao.getRepositoryPreferences(repoId3) ?: fail()
assertTrue(repoPrefs2.weight < repoPrefs3.weight)
assertTrue(repoPrefs3.weight < repoPrefs1.weight)
// app from repo with highest weight is returned (app1)
getItems { apps ->
assertEquals(1, apps.size)
assertEquals(packageName, apps[0].packageName)
assertEquals(app1, apps[0])
}
}
@Test
fun testOnlyFromGivenCategories() {
// insert three apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
appDao.insert(repoId, packageName3, app3, locales)
// only two apps are in category B
listOf(
appDao.getAppListItemsByName("B").getOrFail(),
appDao.getAppListItemsByLastUpdated("B").getOrFail(),
).forEach { apps ->
assertEquals(2, apps.size)
assertNotEquals(packageName2, apps[0].packageName)
assertNotEquals(packageName2, apps[1].packageName)
}
// no app is in category C
listOf(
appDao.getAppListItemsByName("C").getOrFail(),
appDao.getAppListItemsByLastUpdated("C").getOrFail(),
).forEach { apps ->
assertEquals(0, apps.size)
}
}
@Test
fun testGetInstalledAppListItems() {
// insert three apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
appDao.insert(repoId, packageName3, app3, locales)
// define packageInfo for each test
@Suppress("DEPRECATION")
val packageInfo1 = PackageInfo().apply {
packageName = packageName1
versionName = getRandomString()
versionCode = Random.nextInt(1, Int.MAX_VALUE)
}
val packageInfo2 = PackageInfo().apply { packageName = packageName2 }
val packageInfo3 = PackageInfo().apply { packageName = packageName3 }
// all apps get returned, if we consider all of them installed
every {
pm.getInstalledPackages(0)
} returns listOf(packageInfo1, packageInfo2, packageInfo3)
assertEquals(3, appDao.getInstalledAppListItems(pm).getOrFail().size)
// one apps get returned, if we consider only that one installed
every { pm.getInstalledPackages(0) } returns listOf(packageInfo1)
appDao.getInstalledAppListItems(pm).getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
// version code and version name gets taken from supplied packageInfo
assertEquals(packageInfo1.getVersionCode(), apps[0].installedVersionCode)
assertEquals(packageInfo1.versionName, apps[0].installedVersionName)
}
// no app gets returned, if we consider none installed
every { pm.getInstalledPackages(0) } returns emptyList()
appDao.getInstalledAppListItems(pm).getOrFail().let { apps ->
assertEquals(0, apps.size)
}
}
/**
* Runs the given block on all getAppListItems* methods.
* Uses category "A" as all apps should be in that.
*/
private fun getItems(alsoInstalled: Boolean = true, block: (List<AppListItem>) -> Unit) {
appDao.getAppListItemsByName().getOrFail().let(block)
appDao.getAppListItemsByName("A").getOrFail().let(block)
appDao.getAppListItemsByLastUpdated().getOrFail().let(block)
appDao.getAppListItemsByLastUpdated("A").getOrFail().let(block)
if (alsoInstalled) {
// everything is always considered to be installed
val packageInfo =
PackageInfo().apply { packageName = this@AppListItemsTest.packageName }
val packageInfo1 = PackageInfo().apply { packageName = packageName1 }
val packageInfo2 = PackageInfo().apply { packageName = packageName2 }
val packageInfo3 = PackageInfo().apply { packageName = packageName3 }
every {
pm.getInstalledPackages(0)
} returns listOf(packageInfo, packageInfo1, packageInfo2, packageInfo3)
appDao.getInstalledAppListItems(pm).getOrFail().let(block)
}
}
private fun assertEquals(expected: MetadataV2, actual: AppListItem) {
assertEquals(expected.name.getBestLocale(locales), actual.name)
assertEquals(expected.summary.getBestLocale(locales), actual.summary)
assertEquals(expected.icon.getBestLocale(locales), actual.getIcon(locales))
}
}

View File

@@ -0,0 +1,298 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.TestUtils.getOrAwaitValue
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.MetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@RunWith(AndroidJUnit4::class)
internal class AppOverviewItemsTest : AppTest() {
@Test
fun testAntiFeatures() {
// insert one apps with without version
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
// without version, anti-features are empty
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertNull(apps[0].antiFeatures)
}
// with one version, the app has those anti-features
val version = getRandomPackageVersionV2(versionCode = 42)
versionDao.insert(repoId, packageName, "1", version, true)
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures)
}
// with two versions, the app has the anti-features of the highest version
val version2 = getRandomPackageVersionV2(versionCode = 23)
versionDao.insert(repoId, packageName, "2", version2, true)
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(version.antiFeatures, apps[0].antiFeatures)
}
// with three versions, the app has the anti-features of the highest version
val version3 = getRandomPackageVersionV2(versionCode = 1337)
versionDao.insert(repoId, packageName, "3", version3, true)
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(version3.antiFeatures, apps[0].antiFeatures)
}
}
@Test
fun testIcons() {
// insert one app
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
// icon is returned correctly
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
}
// insert same app into another repo
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName, app2, locales)
// now icon is returned from app in second repo
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales))
}
}
@Test
fun testLimit() {
// insert three apps
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
appDao.insert(repoId, packageName3, app3, locales)
// limit is respected
for (i in 0..3) assertEquals(i, appDao.getAppOverviewItems(i).getOrFail().size)
assertEquals(3, appDao.getAppOverviewItems(42).getOrFail().size)
}
@Test
fun testGetByRepoWeight() {
// insert one app with one version
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, app1, locales)
versionDao.insert(repoId, packageName, "1", getRandomPackageVersionV2(2), true)
// app is returned correctly
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app1, apps[0])
}
// add another app without version
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName, app2, locales)
// now second app from second repo is returned
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(1, apps.size)
assertEquals(app2, apps[0])
}
}
@Test
fun testSortOrder() {
// insert two apps with one version each
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
versionDao.insert(repoId, packageName1, "1", getRandomPackageVersionV2(), true)
versionDao.insert(repoId, packageName2, "2", getRandomPackageVersionV2(), true)
// icons of both apps are returned correctly
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(2, apps.size)
// app 2 is first, because has icon and summary
assertEquals(packageName2, apps[0].packageName)
assertEquals(icons2, apps[0].localizedIcon?.toLocalizedFileV2())
// app 1 is next, because has icon
assertEquals(packageName1, apps[1].packageName)
assertEquals(icons1, apps[1].localizedIcon?.toLocalizedFileV2())
}
// app without icon is returned last
appDao.insert(repoId, packageName3, app3)
versionDao.insert(repoId, packageName3, "3", getRandomPackageVersionV2(), true)
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(3, apps.size)
assertEquals(packageName2, apps[0].packageName)
assertEquals(packageName1, apps[1].packageName)
assertEquals(packageName3, apps[2].packageName)
assertEquals(emptyList(), apps[2].localizedIcon)
}
// app1b is the same as app1 (but in another repo) and thus will not be shown again
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val app1b = app1.copy(name = name2, icon = icons2, summary = name2)
appDao.insert(repoId2, packageName1, app1b)
// note that we don't insert a version here
assertEquals(3, appDao.getAppOverviewItems().getOrFail().size)
// app3b is the same as app3, but has an icon, so is not last anymore
val app3b = app3.copy(icon = icons2)
appDao.insert(repoId2, packageName3, app3b)
// note that we don't insert a version here
appDao.getAppOverviewItems().getOrFail().let { apps ->
assertEquals(3, apps.size)
assertEquals(packageName3, apps[0].packageName)
assertEquals(emptyList(), apps[0].antiFeatureKeys)
assertEquals(packageName2, apps[1].packageName)
assertEquals(packageName1, apps[2].packageName)
}
}
@Test
fun testSortOrderWithCategories() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
versionDao.insert(repoId, packageName1, "1", getRandomPackageVersionV2(), true)
versionDao.insert(repoId, packageName2, "2", getRandomPackageVersionV2(), true)
// icons of both apps are returned correctly
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
assertEquals(2, apps.size)
// app 2 is first, because has icon and summary
assertEquals(packageName2, apps[0].packageName)
assertEquals(icons2, apps[0].localizedIcon?.toLocalizedFileV2())
// app 1 is next, because has icon
assertEquals(packageName1, apps[1].packageName)
assertEquals(icons1, apps[1].localizedIcon?.toLocalizedFileV2())
}
// only one app is returned for category B
assertEquals(1, appDao.getAppOverviewItems("B").getOrFail().size)
// app without icon is returned last
appDao.insert(repoId, packageName3, app3)
versionDao.insert(repoId, packageName3, "3", getRandomPackageVersionV2(), true)
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
assertEquals(3, apps.size)
assertEquals(packageName2, apps[0].packageName)
assertEquals(packageName1, apps[1].packageName)
assertEquals(packageName3, apps[2].packageName)
assertEquals(emptyList(), apps[2].localizedIcon)
}
// app1b is the same as app1 (but in another repo) and thus will not be shown again
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
val app1b = app1.copy(name = name2, icon = icons2, summary = name2)
appDao.insert(repoId2, packageName1, app1b)
// note that we don't insert a version here
assertEquals(3, appDao.getAppOverviewItems("A").getOrFail().size)
// app3b is the same as app3, but has an icon, so is not last anymore
val app3b = app3.copy(icon = icons2)
appDao.insert(repoId2, packageName3, app3b)
// note that we don't insert a version here
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
assertEquals(3, apps.size)
assertEquals(packageName3, apps[0].packageName)
assertEquals(emptyList(), apps[0].antiFeatureKeys)
assertEquals(packageName2, apps[1].packageName)
assertEquals(packageName1, apps[2].packageName)
}
// only two apps are returned for category B
assertEquals(2, appDao.getAppOverviewItems("B").getOrFail().size)
}
@Test
fun testOnlyFromEnabledRepos() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName3, app3, locales)
// 3 apps from 2 repos
assertEquals(3, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
// only 1 app after disabling first repo
repoDao.setRepositoryEnabled(repoId, false)
assertEquals(1, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
assertEquals(1, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(1, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
// no more apps after disabling all repos
repoDao.setRepositoryEnabled(repoId2, false)
assertEquals(0, appDao.getAppOverviewItems().getOrAwaitValue()?.size)
assertEquals(0, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size)
assertEquals(0, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size)
}
@Test
fun testGetAppOverviewItem() {
// insert three apps into two repos
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName1, app1, locales)
appDao.insert(repoId, packageName2, app2, locales)
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName3, app3, locales)
// each app gets returned properly
assertEquals(app1, appDao.getAppOverviewItem(repoId, packageName1))
assertEquals(app2, appDao.getAppOverviewItem(repoId, packageName2))
assertEquals(app3, appDao.getAppOverviewItem(repoId2, packageName3))
// apps don't get returned from wrong repos
assertNull(appDao.getAppOverviewItem(repoId2, packageName1))
assertNull(appDao.getAppOverviewItem(repoId2, packageName2))
assertNull(appDao.getAppOverviewItem(repoId, packageName3))
}
@Test
fun testGetAppOverviewItemWithIcons() {
// insert one app (with overlapping icons) into two repos
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId1, packageName, app1, locales)
appDao.insert(repoId2, packageName, app2, locales)
// each app gets returned properly
assertEquals(app1, appDao.getAppOverviewItem(repoId1, packageName))
assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName))
// disable second repo
repoDao.setRepositoryEnabled(repoId2, false)
// each app still gets returned properly
assertEquals(app1, appDao.getAppOverviewItem(repoId1, packageName))
assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName))
}
private fun assertEquals(expected: MetadataV2, actual: AppOverviewItem?) {
assertNotNull(actual)
assertEquals(expected.added, actual.added)
assertEquals(expected.lastUpdated, actual.lastUpdated)
assertEquals(expected.name.getBestLocale(locales), actual.name)
assertEquals(expected.summary.getBestLocale(locales), actual.summary)
assertEquals(expected.summary.getBestLocale(locales), actual.summary)
assertEquals(expected.icon.getBestLocale(locales), actual.getIcon(locales))
}
}

View File

@@ -0,0 +1,47 @@
package org.fdroid.database
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomFileV2
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestUtils.sort
import org.junit.Rule
internal abstract class AppTest : DbTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
protected val packageName = getRandomString()
protected val packageName1 = getRandomString()
protected val packageName2 = getRandomString()
protected val packageName3 = getRandomString()
protected val name1 = mapOf("en-US" to "1")
protected val name2 = mapOf("en-US" to "2")
protected val name3 = mapOf("en-US" to "3")
// it is important for testing that the icons are sharing at least one locale
protected val icons1 = mapOf("en-US" to getRandomFileV2(), "bar" to getRandomFileV2())
protected val icons2 = mapOf("en-US" to getRandomFileV2(), "42" to getRandomFileV2())
protected val app1 = getRandomMetadataV2().copy(
name = name1,
icon = icons1,
summary = null,
lastUpdated = 10,
categories = listOf("A", "B")
).sort()
protected val app2 = getRandomMetadataV2().copy(
name = name2,
icon = icons2,
summary = name2,
lastUpdated = 20,
categories = listOf("A")
).sort()
protected val app3 = getRandomMetadataV2().copy(
name = name3,
icon = null,
summary = name3,
lastUpdated = 30,
categories = listOf("A", "B")
).sort()
}

View File

@@ -0,0 +1,120 @@
package org.fdroid.database
import android.content.Context
import android.content.res.AssetManager
import androidx.core.os.LocaleListCompat
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import io.mockk.every
import io.mockk.mockkObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.fdroid.database.TestUtils.assertRepoEquals
import org.fdroid.database.TestUtils.toMetadataV2
import org.fdroid.database.TestUtils.toPackageVersionV2
import org.fdroid.index.v1.IndexV1StreamProcessor
import org.fdroid.index.v2.IndexV2
import org.fdroid.index.v2.IndexV2FullStreamProcessor
import org.fdroid.test.TestUtils.sort
import org.fdroid.test.TestUtils.sorted
import org.fdroid.test.VerifierConstants.CERTIFICATE
import org.junit.After
import org.junit.Before
import java.io.IOException
import java.util.Locale
import kotlin.test.assertEquals
import kotlin.test.fail
@OptIn(ExperimentalCoroutinesApi::class)
internal abstract class DbTest {
internal lateinit var repoDao: RepositoryDaoInt
internal lateinit var appDao: AppDaoInt
internal lateinit var appPrefsDao: AppPrefsDaoInt
internal lateinit var versionDao: VersionDaoInt
internal lateinit var db: FDroidDatabaseInt
private val testCoroutineDispatcher = Dispatchers.Unconfined
protected val context: Context = getApplicationContext()
protected val assets: AssetManager = context.resources.assets
protected val locales = LocaleListCompat.create(Locale.US)
@Before
open fun createDb() {
db = Room.inMemoryDatabaseBuilder(context, FDroidDatabaseInt::class.java)
.allowMainThreadQueries()
.build()
repoDao = db.getRepositoryDao()
appDao = db.getAppDao()
appPrefsDao = db.getAppPrefsDao()
versionDao = db.getVersionDao()
mockkObject(FDroidDatabaseHolder)
every { FDroidDatabaseHolder.dispatcher } returns testCoroutineDispatcher
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
protected fun streamIndexV1IntoDb(
indexAssetPath: String,
address: String = "https://f-droid.org/repo",
certificate: String = CERTIFICATE,
lastTimestamp: Long = -1,
): Long {
val repoId = db.getRepositoryDao().insertEmptyRepo(address)
val streamReceiver = DbV1StreamReceiver(db, repoId) { true }
val indexProcessor = IndexV1StreamProcessor(streamReceiver, certificate, lastTimestamp)
db.runInTransaction {
assets.open(indexAssetPath).use { indexStream ->
indexProcessor.process(indexStream)
}
}
return repoId
}
protected fun streamIndexV2IntoDb(
indexAssetPath: String,
address: String = "https://f-droid.org/repo",
version: Long = 42L,
certificate: String = CERTIFICATE,
): Long {
val repoId = db.getRepositoryDao().insertEmptyRepo(address)
val streamReceiver = DbV2StreamReceiver(db, repoId) { true }
val indexProcessor = IndexV2FullStreamProcessor(streamReceiver, certificate)
db.runInTransaction {
assets.open(indexAssetPath).use { indexStream ->
indexProcessor.process(version, indexStream) {}
}
}
return repoId
}
/**
* Asserts that data associated with the given [repoId] is equal to the given [index].
*/
protected fun assertDbEquals(repoId: Long, index: IndexV2) {
val repo = repoDao.getRepository(repoId) ?: fail()
val sortedIndex = index.sorted()
assertRepoEquals(sortedIndex.repo, repo)
assertEquals(sortedIndex.packages.size, appDao.countApps(), "number of packages")
sortedIndex.packages.forEach { (packageName, packageV2) ->
assertEquals(
packageV2.metadata,
appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort()
)
val versions = versionDao.getAppVersions(repoId, packageName).map {
it.toPackageVersionV2()
}.associateBy { it.file.sha256 }
assertEquals(packageV2.versions.size, versions.size, "number of versions")
packageV2.versions.forEach { (versionId, packageVersionV2) ->
val version = versions[versionId] ?: fail()
assertEquals(packageVersionV2, version)
}
}
}
}

View File

@@ -0,0 +1,82 @@
package org.fdroid.database
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockk
import org.fdroid.index.RELEASE_CHANNEL_BETA
import org.fdroid.test.TestDataMidV2
import org.fdroid.test.TestDataMinV2
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertNull
@RunWith(AndroidJUnit4::class)
internal class DbUpdateCheckerTest : DbTest() {
private lateinit var updateChecker: DbUpdateChecker
private val packageManager: PackageManager = mockk()
private val packageInfo = PackageInfo().apply {
packageName = TestDataMinV2.packageName
@Suppress("DEPRECATION")
versionCode = 0
}
@Before
override fun createDb() {
super.createDb()
every { packageManager.systemAvailableFeatures } returns emptyArray()
updateChecker = DbUpdateChecker(db, packageManager)
}
@Test
fun testSuggestedVersion() {
val repoId = streamIndexV2IntoDb("index-min-v2.json")
every {
packageManager.getPackageInfo(packageInfo.packageName, any())
} returns packageInfo
val appVersion = updateChecker.getSuggestedVersion(packageInfo.packageName)
val expectedVersion = TestDataMinV2.version.toVersion(
repoId = repoId,
packageName = packageInfo.packageName,
versionId = TestDataMinV2.version.file.sha256,
isCompatible = true,
)
assertEquals(appVersion!!.version, expectedVersion)
}
@Test
fun testSuggestedVersionRespectsReleaseChannels() {
streamIndexV2IntoDb("index-mid-v2.json")
every { packageManager.getPackageInfo(packageInfo.packageName, any()) } returns null
// no suggestion version, because all beta
val appVersion1 = updateChecker.getSuggestedVersion(packageInfo.packageName)
assertNull(appVersion1)
// now suggests only available version
val appVersion2 = updateChecker.getSuggestedVersion(
packageName = packageInfo.packageName,
releaseChannels = listOf(RELEASE_CHANNEL_BETA),
preferredSigner = TestDataMidV2.version1_2.signer!!.sha256[0],
)
assertEquals(TestDataMidV2.version1_2.versionCode, appVersion2!!.version.versionCode)
}
@Test
fun testGetUpdatableApps() {
streamIndexV2IntoDb("index-min-v2.json")
every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo)
val appVersions = updateChecker.getUpdatableApps()
assertEquals(1, appVersions.size)
assertEquals(0, appVersions[0].installedVersionCode)
assertEquals(TestDataMinV2.packageName, appVersions[0].packageName)
assertEquals(TestDataMinV2.version.file.sha256, appVersions[0].update.version.versionId)
}
}

View File

@@ -0,0 +1,134 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.SerializationException
import org.apache.commons.io.input.CountingInputStream
import org.fdroid.index.IndexConverter
import org.fdroid.index.v1.IndexV1StreamProcessor
import org.fdroid.index.v1.IndexV1StreamReceiver
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import org.fdroid.test.TestDataEmptyV1
import org.fdroid.test.TestDataMaxV1
import org.fdroid.test.TestDataMidV1
import org.fdroid.test.TestDataMinV1
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
internal class IndexV1InsertTest : DbTest() {
private val indexConverter = IndexConverter()
@Test
fun testStreamEmptyIntoDb() {
val repoId = streamIndex("index-empty-v1.json")
assertEquals(1, repoDao.getRepositories().size)
val index = indexConverter.toIndexV2(TestDataEmptyV1.index)
assertDbEquals(repoId, index)
}
@Test
fun testStreamMinIntoDb() {
val repoId = streamIndex("index-min-v1.json")
assertTrue(repoDao.getRepositories().size == 1)
val index = indexConverter.toIndexV2(TestDataMinV1.index)
assertDbEquals(repoId, index)
}
@Test
fun testStreamMidIntoDb() {
val repoId = streamIndex("index-mid-v1.json")
assertTrue(repoDao.getRepositories().size == 1)
val index = indexConverter.toIndexV2(TestDataMidV1.index)
assertDbEquals(repoId, index)
}
@Test
fun testStreamMaxIntoDb() {
val repoId = streamIndex("index-max-v1.json")
assertTrue(repoDao.getRepositories().size == 1)
val index = indexConverter.toIndexV2(TestDataMaxV1.index)
assertDbEquals(repoId, index)
}
private fun streamIndex(path: String): Long {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val streamReceiver = TestStreamReceiver(repoId)
val indexProcessor = IndexV1StreamProcessor(streamReceiver, null, -1)
db.runInTransaction {
assets.open(path).use { indexStream ->
indexProcessor.process(indexStream)
}
}
return repoId
}
@Test
fun testExceptionWhileStreamingDoesNotSaveIntoDb() {
val cIn = CountingInputStream(assets.open("index-max-v1.json"))
assertFailsWith<SerializationException> {
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo")
val streamReceiver = TestStreamReceiver(repoId) {
if (cIn.byteCount > 0) throw SerializationException()
}
val indexProcessor = IndexV1StreamProcessor(streamReceiver, null, -1)
cIn.use { indexStream ->
indexProcessor.process(indexStream)
}
}
}
assertTrue(repoDao.getRepositories().isEmpty())
assertTrue(appDao.countApps() == 0)
assertTrue(appDao.countLocalizedFiles() == 0)
assertTrue(appDao.countLocalizedFileLists() == 0)
assertTrue(versionDao.countAppVersions() == 0)
assertTrue(versionDao.countVersionedStrings() == 0)
}
@Suppress("DEPRECATION")
inner class TestStreamReceiver(
repoId: Long,
private val callback: () -> Unit = {},
) : IndexV1StreamReceiver {
private val streamReceiver = DbV1StreamReceiver(db, repoId) { true }
override fun receive(repo: RepoV2, version: Long, certificate: String?) {
streamReceiver.receive(repo, version, certificate)
callback()
}
override fun receive(packageName: String, m: MetadataV2) {
streamReceiver.receive(packageName, m)
callback()
}
override fun receive(packageName: String, v: Map<String, PackageVersionV2>) {
streamReceiver.receive(packageName, v)
callback()
}
override fun updateRepo(
antiFeatures: Map<String, AntiFeatureV2>,
categories: Map<String, CategoryV2>,
releaseChannels: Map<String, ReleaseChannelV2>,
) {
streamReceiver.updateRepo(antiFeatures, categories, releaseChannels)
callback()
}
override fun updateAppMetadata(packageName: String, preferredSigner: String?) {
streamReceiver.updateAppMetadata(packageName, preferredSigner)
callback()
}
}
}

View File

@@ -0,0 +1,314 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.SerializationException
import org.fdroid.index.IndexParser
import org.fdroid.index.parseV2
import org.fdroid.index.v2.IndexV2
import org.fdroid.index.v2.IndexV2DiffStreamProcessor
import org.fdroid.test.TestDataMaxV2
import org.fdroid.test.TestDataMidV2
import org.fdroid.test.TestDataMinV2
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlin.test.assertFailsWith
@RunWith(AndroidJUnit4::class)
internal class IndexV2DiffTest : DbTest() {
@Test
@Ignore("use for testing specific index on demand")
fun testBrokenIndexDiff() {
val endPath = "tmp/index-end.json"
val endIndex = IndexParser.parseV2(assets.open(endPath))
testDiff(
startPath = "tmp/index-start.json",
diffPath = "tmp/diff.json",
endIndex = endIndex,
)
}
@Test
fun testEmptyToMin() = testDiff(
startPath = "index-empty-v2.json",
diffPath = "diff-empty-min/23.json",
endIndex = TestDataMinV2.index,
)
@Test
fun testEmptyToMid() = testDiff(
startPath = "index-empty-v2.json",
diffPath = "diff-empty-mid/23.json",
endIndex = TestDataMidV2.index,
)
@Test
fun testEmptyToMax() = testDiff(
startPath = "index-empty-v2.json",
diffPath = "diff-empty-max/23.json",
endIndex = TestDataMaxV2.index,
)
@Test
fun testMinToMid() = testDiff(
startPath = "index-min-v2.json",
diffPath = "diff-empty-mid/42.json",
endIndex = TestDataMidV2.index,
)
@Test
fun testMinToMax() = testDiff(
startPath = "index-min-v2.json",
diffPath = "diff-empty-max/42.json",
endIndex = TestDataMaxV2.index,
)
@Test
fun testMidToMax() = testDiff(
startPath = "index-mid-v2.json",
diffPath = "diff-empty-max/1337.json",
endIndex = TestDataMaxV2.index,
)
@Test
fun testMinRemoveApp() {
val diffJson = """{
"packages": {
"org.fdroid.min1": null
}
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index.copy(packages = emptyMap()),
)
}
@Test
fun testMinNoMetadataRemoveVersion() {
val diffJson = """{
"packages": {
"org.fdroid.min1": {
"metadata": {
"added": 0
},
"versions": {
"824a109b2352138c3699760e1683385d0ed50ce526fc7982f8d65757743374bf": null
}
}
}
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index.copy(
packages = TestDataMinV2.index.packages.mapValues {
it.value.copy(versions = emptyMap())
}
),
)
}
@Test
fun testMinNoVersionsUnknownKey() {
val diffJson = """{
"packages": {
"org.fdroid.min1": {
"metadata": {
"added": 42
},
"unknownKey": "should get ignored"
}
}
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index.copy(
packages = TestDataMinV2.index.packages.mapValues {
it.value.copy(metadata = it.value.metadata.copy(added = 42))
}
),
)
}
@Test
fun testMinRemoveMetadata() {
val diffJson = """{
"packages": {
"org.fdroid.min1": {
"metadata": null
}
},
"unknownKey": "should get ignored"
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index.copy(
packages = emptyMap()
),
)
}
@Test
fun testMinRemoveVersions() {
val diffJson = """{
"packages": {
"org.fdroid.min1": {
"versions": null
}
}
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index.copy(
packages = TestDataMinV2.index.packages.mapValues {
it.value.copy(versions = emptyMap())
}
),
)
}
@Test
fun testMinNoMetadataNoVersion() {
val diffJson = """{
"packages": {
"org.fdroid.min1": {
}
}
}""".trimIndent()
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffJson,
endIndex = TestDataMinV2.index,
)
}
@Test
fun testAppDenyKeyList() {
val diffRepoIdJson = """{
"packages": {
"org.fdroid.min1": {
"metadata": {
"repoId": 1
}
}
}
}""".trimIndent()
assertFailsWith<SerializationException> {
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffRepoIdJson,
endIndex = TestDataMinV2.index,
)
}
val diffPackageNameJson = """{
"packages": {
"org.fdroid.min1": {
"metadata": {
"packageName": "foo"
}
}
}
}""".trimIndent()
assertFailsWith<SerializationException> {
testJsonDiff(
startPath = "index-min-v2.json",
diff = diffPackageNameJson,
endIndex = TestDataMinV2.index,
)
}
}
@Test
fun testVersionsDenyKeyList() {
assertFailsWith<SerializationException> {
testJsonDiff(
startPath = "index-min-v2.json",
diff = getMinVersionJson(""""packageName": "foo""""),
endIndex = TestDataMinV2.index,
)
}
assertFailsWith<SerializationException> {
testJsonDiff(
startPath = "index-min-v2.json",
diff = getMinVersionJson(""""repoId": 1"""),
endIndex = TestDataMinV2.index,
)
}
assertFailsWith<SerializationException> {
testJsonDiff(
startPath = "index-min-v2.json",
diff = getMinVersionJson(""""versionId": "bar""""),
endIndex = TestDataMinV2.index,
)
}
}
private fun getMinVersionJson(insert: String) = """{
"packages": {
"org.fdroid.min1": {
"versions": {
"824a109b2352138c3699760e1683385d0ed50ce526fc7982f8d65757743374bf": {
$insert
}
}
}
}""".trimIndent()
@Test
fun testMidRemoveScreenshots() {
val diffRepoIdJson = """{
"packages": {
"org.fdroid.fdroid": {
"metadata": {
"screenshots": null
}
}
}
}""".trimIndent()
val fdroidPackage = TestDataMidV2.packages["org.fdroid.fdroid"]!!.copy(
metadata = TestDataMidV2.packages["org.fdroid.fdroid"]!!.metadata.copy(
screenshots = null,
)
)
testJsonDiff(
startPath = "index-mid-v2.json",
diff = diffRepoIdJson,
endIndex = TestDataMidV2.index.copy(
packages = mapOf(
TestDataMidV2.packageName1 to TestDataMidV2.app1,
TestDataMidV2.packageName2 to fdroidPackage,
)
),
)
}
private fun testJsonDiff(startPath: String, diff: String, endIndex: IndexV2) {
testDiff(startPath, ByteArrayInputStream(diff.toByteArray()), endIndex)
}
private fun testDiff(startPath: String, diffPath: String, endIndex: IndexV2) {
testDiff(startPath, assets.open(diffPath), endIndex)
}
private fun testDiff(startPath: String, diffStream: InputStream, endIndex: IndexV2) {
// stream start index into the DB
val repoId = streamIndexV2IntoDb(startPath)
// apply diff stream to the DB
val streamReceiver = DbV2DiffStreamReceiver(db, repoId) { true }
val streamProcessor = IndexV2DiffStreamProcessor(streamReceiver)
db.runInTransaction {
streamProcessor.process(42, diffStream) {}
}
// assert that changed DB data is equal to given endIndex
assertDbEquals(repoId, endIndex)
}
}

View File

@@ -0,0 +1,81 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.SerializationException
import org.apache.commons.io.input.CountingInputStream
import org.fdroid.CompatibilityChecker
import org.fdroid.index.v2.IndexV2FullStreamProcessor
import org.fdroid.test.TestDataEmptyV2
import org.fdroid.test.TestDataMaxV2
import org.fdroid.test.TestDataMidV2
import org.fdroid.test.TestDataMinV2
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
internal class IndexV2InsertTest : DbTest() {
@Test
fun testStreamEmptyIntoDb() {
val repoId = streamIndexV2IntoDb("index-empty-v2.json")
assertEquals(1, repoDao.getRepositories().size)
assertDbEquals(repoId, TestDataEmptyV2.index)
}
@Test
fun testStreamMinIntoDb() {
val repoId = streamIndexV2IntoDb("index-min-v2.json")
assertEquals(1, repoDao.getRepositories().size)
assertDbEquals(repoId, TestDataMinV2.index)
}
@Test
fun testStreamMinReorderedIntoDb() {
val repoId = streamIndexV2IntoDb("index-min-reordered-v2.json")
assertEquals(1, repoDao.getRepositories().size)
assertDbEquals(repoId, TestDataMinV2.index)
}
@Test
fun testStreamMidIntoDb() {
val repoId = streamIndexV2IntoDb("index-mid-v2.json")
assertEquals(1, repoDao.getRepositories().size)
assertDbEquals(repoId, TestDataMidV2.index)
}
@Test
fun testStreamMaxIntoDb() {
val repoId = streamIndexV2IntoDb("index-max-v2.json")
assertEquals(1, repoDao.getRepositories().size)
assertDbEquals(repoId, TestDataMaxV2.index)
}
@Test
fun testExceptionWhileStreamingDoesNotSaveIntoDb() {
val cIn = CountingInputStream(assets.open("index-max-v2.json"))
val compatibilityChecker = CompatibilityChecker {
if (cIn.byteCount > 0) throw SerializationException()
true
}
assertFailsWith<SerializationException> {
db.runInTransaction {
val repoId = db.getRepositoryDao().insertEmptyRepo("http://example.org")
val streamReceiver = DbV2StreamReceiver(db, repoId, compatibilityChecker)
val indexProcessor = IndexV2FullStreamProcessor(streamReceiver, "")
cIn.use { indexStream ->
indexProcessor.process(42, indexStream) {}
}
}
}
assertTrue(repoDao.getRepositories().isEmpty())
assertTrue(appDao.countApps() == 0)
assertTrue(appDao.countLocalizedFiles() == 0)
assertTrue(appDao.countLocalizedFileLists() == 0)
assertTrue(versionDao.countAppVersions() == 0)
assertTrue(versionDao.countVersionedStrings() == 0)
}
}

View File

@@ -0,0 +1,269 @@
package org.fdroid.database
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.database.TestUtils.assertRepoEquals
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestUtils.orNull
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class RepositoryDaoTest : DbTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun testInsertInitialRepository() {
val repo = InitialRepository(
name = getRandomString(),
address = getRandomString(),
description = getRandomString(),
certificate = getRandomString(),
version = Random.nextLong(),
enabled = Random.nextBoolean(),
weight = Random.nextInt(),
)
val repoId = repoDao.insert(repo)
val actualRepo = repoDao.getRepository(repoId) ?: fail()
assertEquals(repo.name, actualRepo.getName(locales))
assertEquals(repo.address, actualRepo.address)
assertEquals(repo.description, actualRepo.getDescription(locales))
assertEquals(repo.certificate, actualRepo.certificate)
assertEquals(repo.version, actualRepo.version)
assertEquals(repo.enabled, actualRepo.enabled)
assertEquals(repo.weight, actualRepo.weight)
assertEquals(-1, actualRepo.timestamp)
assertEquals(emptyList(), actualRepo.mirrors)
assertEquals(emptyList(), actualRepo.userMirrors)
assertEquals(emptyList(), actualRepo.disabledMirrors)
assertEquals(listOf(org.fdroid.download.Mirror(repo.address)), actualRepo.getMirrors())
assertEquals(emptyList(), actualRepo.antiFeatures)
assertEquals(emptyList(), actualRepo.categories)
assertEquals(emptyList(), actualRepo.releaseChannels)
assertNull(actualRepo.formatVersion)
assertNull(actualRepo.repository.icon)
assertNull(actualRepo.lastUpdated)
assertNull(actualRepo.webBaseUrl)
}
@Test
fun testInsertEmptyRepo() {
// insert empty repo
val address = getRandomString()
val username = getRandomString().orNull()
val password = getRandomString().orNull()
val repoId = repoDao.insertEmptyRepo(address, username, password)
// check that repo got inserted as expected
val actualRepo = repoDao.getRepository(repoId) ?: fail()
assertEquals(address, actualRepo.address)
assertEquals(username, actualRepo.username)
assertEquals(password, actualRepo.password)
assertEquals(-1, actualRepo.timestamp)
assertEquals(listOf(org.fdroid.download.Mirror(address)), actualRepo.getMirrors())
assertEquals(emptyList(), actualRepo.antiFeatures)
assertEquals(emptyList(), actualRepo.categories)
assertEquals(emptyList(), actualRepo.releaseChannels)
assertNull(actualRepo.formatVersion)
assertNull(actualRepo.repository.icon)
assertNull(actualRepo.lastUpdated)
assertNull(actualRepo.webBaseUrl)
}
@Test
fun insertAndDeleteTwoRepos() {
// insert first repo
val repo1 = getRandomRepo()
val repoId1 = repoDao.insertOrReplace(repo1)
// check that first repo got added and retrieved as expected
repoDao.getRepositories().let { repos ->
assertEquals(1, repos.size)
assertRepoEquals(repo1, repos[0])
}
val repositoryPreferences1 = repoDao.getRepositoryPreferences(repoId1)
assertEquals(repoId1, repositoryPreferences1?.repoId)
// insert second repo
val repo2 = getRandomRepo()
val repoId2 = repoDao.insertOrReplace(repo2)
// check that both repos got added and retrieved as expected
listOf(
repoDao.getRepositories().sortedBy { it.repoId },
repoDao.getLiveRepositories().getOrFail().sortedBy { it.repoId },
).forEach { repos ->
assertEquals(2, repos.size)
assertRepoEquals(repo1, repos[0])
assertRepoEquals(repo2, repos[1])
}
val repositoryPreferences2 = repoDao.getRepositoryPreferences(repoId2)
assertEquals(repoId2, repositoryPreferences2?.repoId)
// second repo has one weight point more than first repo
assertEquals(repositoryPreferences1?.weight?.plus(1), repositoryPreferences2?.weight)
// remove first repo and check that the database only returns one
repoDao.deleteRepository(repoId1)
listOf(
repoDao.getRepositories(),
repoDao.getLiveRepositories().getOrFail(),
).forEach { repos ->
assertEquals(1, repos.size)
assertRepoEquals(repo2, repos[0])
}
assertNull(repoDao.getRepositoryPreferences(repoId1))
// remove second repo and check that all associated data got removed as well
repoDao.deleteRepository(repoId2)
assertEquals(0, repoDao.getRepositories().size)
assertEquals(0, repoDao.countMirrors())
assertEquals(0, repoDao.countAntiFeatures())
assertEquals(0, repoDao.countCategories())
assertEquals(0, repoDao.countReleaseChannels())
assertNull(repoDao.getRepositoryPreferences(repoId2))
}
@Test
fun insertTwoReposAndClearAll() {
val repo1 = getRandomRepo()
val repo2 = getRandomRepo()
repoDao.insertOrReplace(repo1)
repoDao.insertOrReplace(repo2)
assertEquals(2, repoDao.getRepositories().size)
assertEquals(2, repoDao.getLiveRepositories().getOrFail().size)
repoDao.clearAll()
assertEquals(0, repoDao.getRepositories().size)
assertEquals(0, repoDao.getLiveRepositories().getOrFail().size)
}
@Test
fun testSetRepositoryEnabled() {
// repo is enabled by default
val repoId = repoDao.insertOrReplace(getRandomRepo())
assertTrue(repoDao.getRepository(repoId)?.enabled ?: fail())
// disabled repo is disabled
repoDao.setRepositoryEnabled(repoId, false)
assertFalse(repoDao.getRepository(repoId)?.enabled ?: fail())
// enabling again works
repoDao.setRepositoryEnabled(repoId, true)
assertTrue(repoDao.getRepository(repoId)?.enabled ?: fail())
}
@Test
fun testUpdateUserMirrors() {
// repo is enabled by default
val repoId = repoDao.insertOrReplace(getRandomRepo())
assertEquals(emptyList(), repoDao.getRepository(repoId)?.userMirrors)
// add user mirrors
val userMirrors = listOf(getRandomString(), getRandomString(), getRandomString())
repoDao.updateUserMirrors(repoId, userMirrors)
val repo = repoDao.getRepository(repoId) ?: fail()
assertEquals(userMirrors, repo.userMirrors)
// user mirrors are part of all mirrors
val userDownloadMirrors = userMirrors.map { org.fdroid.download.Mirror(it) }
assertTrue(repo.getMirrors().containsAll(userDownloadMirrors))
// remove user mirrors
repoDao.updateUserMirrors(repoId, emptyList())
assertEquals(emptyList(), repoDao.getRepository(repoId)?.userMirrors)
}
@Test
fun testUpdateUsernameAndPassword() {
// repo has no username or password initially
val repoId = repoDao.insertOrReplace(getRandomRepo())
repoDao.getRepository(repoId)?.let { repo ->
assertEquals(null, repo.username)
assertEquals(null, repo.password)
} ?: fail()
// add user name and password
val username = getRandomString().orNull()
val password = getRandomString().orNull()
repoDao.updateUsernameAndPassword(repoId, username, password)
repoDao.getRepository(repoId)?.let { repo ->
assertEquals(username, repo.username)
assertEquals(password, repo.password)
} ?: fail()
}
@Test
fun testUpdateDisabledMirrors() {
// repo has no username or password initially
val repoId = repoDao.insertOrReplace(getRandomRepo())
repoDao.getRepository(repoId)?.let { repo ->
assertEquals(null, repo.username)
assertEquals(null, repo.password)
} ?: fail()
// add user name and password
val username = getRandomString().orNull()
val password = getRandomString().orNull()
repoDao.updateUsernameAndPassword(repoId, username, password)
repoDao.getRepository(repoId)?.let { repo ->
assertEquals(username, repo.username)
assertEquals(password, repo.password)
} ?: fail()
}
@Test
fun clearingRepoRemovesAllAssociatedData() {
// insert one repo with one app with one version
val repoId = repoDao.insertOrReplace(getRandomRepo())
val repositoryPreferences = repoDao.getRepositoryPreferences(repoId)
val packageName = getRandomString()
val versionId = getRandomString()
appDao.insert(repoId, packageName, getRandomMetadataV2())
val packageVersion = getRandomPackageVersionV2()
versionDao.insert(repoId, packageName, versionId, packageVersion, Random.nextBoolean())
// data is there as expected
assertEquals(1, repoDao.getRepositories().size)
assertEquals(1, appDao.getAppMetadata().size)
assertEquals(1, versionDao.getAppVersions(repoId, packageName).size)
assertTrue(versionDao.getVersionedStrings(repoId, packageName).isNotEmpty())
// clearing the repo removes apps and versions
repoDao.clear(repoId)
assertEquals(1, repoDao.getRepositories().size)
assertEquals(0, appDao.countApps())
assertEquals(0, appDao.countLocalizedFiles())
assertEquals(0, appDao.countLocalizedFileLists())
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
// preferences are not touched by clearing
assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId))
}
@Test
fun certGetsUpdated() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
assertEquals(1, repoDao.getRepositories().size)
assertEquals(null, repoDao.getRepositories()[0].certificate)
val cert = getRandomString()
repoDao.updateRepository(repoId, cert)
assertEquals(1, repoDao.getRepositories().size)
assertEquals(cert, repoDao.getRepositories()[0].certificate)
}
}

View File

@@ -0,0 +1,241 @@
package org.fdroid.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.fdroid.database.TestUtils.assertRepoEquals
import org.fdroid.index.v2.AntiFeatureV2
import org.fdroid.index.v2.CategoryV2
import org.fdroid.index.v2.ReleaseChannelV2
import org.fdroid.index.v2.RepoV2
import org.fdroid.test.DiffUtils.applyDiff
import org.fdroid.test.DiffUtils.randomDiff
import org.fdroid.test.TestRepoUtils.getRandomFileV2
import org.fdroid.test.TestRepoUtils.getRandomLocalizedTextV2
import org.fdroid.test.TestRepoUtils.getRandomMirror
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomMap
import org.fdroid.test.TestUtils.getRandomString
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
import kotlin.test.assertEquals
/**
* Tests that repository diffs get applied to the database correctly.
*/
@RunWith(AndroidJUnit4::class)
internal class RepositoryDiffTest : DbTest() {
private val j = Json
@Test
fun timestampDiff() {
val repo = getRandomRepo()
val updateTimestamp = repo.timestamp + 1
val json = """
{
"timestamp": $updateTimestamp
}""".trimIndent()
testDiff(repo, json) { repos ->
assertEquals(updateTimestamp, repos[0].timestamp)
assertRepoEquals(repo.copy(timestamp = updateTimestamp), repos[0])
}
}
@Test
fun timestampDiffTwoReposInDb() {
// insert repo
val repo = getRandomRepo()
repoDao.insertOrReplace(repo)
// insert another repo before updating
repoDao.insertOrReplace(getRandomRepo())
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories().sortedBy { it.repoId }
assertEquals(2, repos.size)
val repoId = repos[0].repoId
val updateTimestamp = Random.nextLong()
val json = """
{
"timestamp": $updateTimestamp
}""".trimIndent()
// decode diff from JSON and update DB with it
val diff = j.parseToJsonElement(json).jsonObject // Json.decodeFromString<RepoDiffV2>(json)
repoDao.updateRepository(repoId, 42, diff)
// fetch repos again and check that the result is as expected
repos = repoDao.getRepositories().sortedBy { it.repoId }
assertEquals(2, repos.size)
assertEquals(repoId, repos[0].repoId)
assertEquals(updateTimestamp, repos[0].timestamp)
assertRepoEquals(repo.copy(timestamp = updateTimestamp), repos[0])
}
@Test
fun mirrorDiff() {
val repo = getRandomRepo()
val updateMirrors = repo.mirrors.toMutableList().apply {
removeLastOrNull()
add(getRandomMirror())
add(getRandomMirror())
}
val json = """
{
"mirrors": ${Json.encodeToString(updateMirrors)}
}""".trimIndent()
testDiff(repo, json) { repos ->
val expectedMirrors = updateMirrors.map { mirror ->
mirror.toMirror(repos[0].repoId)
}.toSet()
assertEquals(expectedMirrors, repos[0].mirrors.toSet())
assertRepoEquals(repo.copy(mirrors = updateMirrors), repos[0])
}
}
@Test
fun descriptionDiff() {
val repo = getRandomRepo().copy(description = mapOf("de" to "foo", "en" to "bar"))
val updateText = if (Random.nextBoolean()) mapOf("de" to null, "en" to "foo") else null
val json = """
{
"description": ${Json.encodeToString(updateText)}
}""".trimIndent()
val expectedText = if (updateText == null) emptyMap() else mapOf("en" to "foo")
testDiff(repo, json) { repos ->
assertEquals(expectedText, repos[0].repository.description)
assertRepoEquals(repo.copy(description = expectedText), repos[0])
}
}
@Test
fun antiFeaturesDiff() {
val repo = getRandomRepo().copy(antiFeatures = getRandomMap {
getRandomString() to AntiFeatureV2(
icon = getRandomFileV2(),
name = getRandomLocalizedTextV2(),
description = getRandomLocalizedTextV2(),
)
})
val antiFeatures = repo.antiFeatures.randomDiff {
AntiFeatureV2(getRandomFileV2(), getRandomLocalizedTextV2(), getRandomLocalizedTextV2())
}
val json = """
{
"antiFeatures": ${Json.encodeToString(antiFeatures)}
}""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.antiFeatures.applyDiff(antiFeatures)
val expectedRepoAntiFeatures =
expectedFeatures.toRepoAntiFeatures(repos[0].repoId)
assertEquals(expectedRepoAntiFeatures.toSet(), repos[0].antiFeatures.toSet())
assertRepoEquals(repo.copy(antiFeatures = expectedFeatures), repos[0])
}
}
@Test
fun antiFeatureKeyChangeDiff() {
val antiFeatureKey = getRandomString()
val antiFeature = AntiFeatureV2(
icon = getRandomFileV2(),
name = getRandomLocalizedTextV2(),
description = getRandomLocalizedTextV2(),
)
val antiFeatures = mapOf(antiFeatureKey to antiFeature)
val repo = getRandomRepo().copy(antiFeatures = antiFeatures)
@Suppress("UNCHECKED_CAST")
val newAntiFeatures = mapOf(antiFeatureKey to antiFeature.copy(
icon = null,
name = getRandomLocalizedTextV2(),
description = getRandomLocalizedTextV2(),
))
val json = """
{
"antiFeatures": {
"$antiFeatureKey": ${Json.encodeToString(newAntiFeatures)}
}
}""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.antiFeatures.applyDiff(antiFeatures)
val expectedRepoAntiFeatures =
expectedFeatures.toRepoAntiFeatures(repos[0].repoId)
assertEquals(expectedRepoAntiFeatures.toSet(), repos[0].antiFeatures.toSet())
assertRepoEquals(repo.copy(antiFeatures = expectedFeatures), repos[0])
}
}
@Test
fun categoriesDiff() {
val repo = getRandomRepo().copy(categories = getRandomMap {
getRandomString() to CategoryV2(
icon = getRandomFileV2(),
name = getRandomLocalizedTextV2(),
description = getRandomLocalizedTextV2(),
)
})
val categories = repo.categories.randomDiff {
CategoryV2(getRandomFileV2(), getRandomLocalizedTextV2(), getRandomLocalizedTextV2())
}
val json = """
{
"categories": ${Json.encodeToString(categories)}
}""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.categories.applyDiff(categories)
val expectedRepoCategories =
expectedFeatures.toRepoCategories(repos[0].repoId)
assertEquals(expectedRepoCategories.toSet(), repos[0].categories.toSet())
assertRepoEquals(repo.copy(categories = expectedFeatures), repos[0])
}
}
@Test
fun releaseChannelsDiff() {
val repo = getRandomRepo().copy(releaseChannels = getRandomMap {
getRandomString() to ReleaseChannelV2(
name = getRandomLocalizedTextV2(),
description = getRandomLocalizedTextV2(),
)
})
val releaseChannels = repo.releaseChannels.randomDiff {
ReleaseChannelV2(getRandomLocalizedTextV2(), getRandomLocalizedTextV2())
}
val json = """
{
"releaseChannels": ${Json.encodeToString(releaseChannels)}
}""".trimIndent()
testDiff(repo, json) { repos ->
val expectedFeatures = repo.releaseChannels.applyDiff(releaseChannels)
val expectedRepoReleaseChannels =
expectedFeatures.toRepoReleaseChannel(repos[0].repoId)
assertEquals(expectedRepoReleaseChannels.toSet(), repos[0].releaseChannels.toSet())
assertRepoEquals(repo.copy(releaseChannels = expectedFeatures), repos[0])
}
}
private fun testDiff(repo: RepoV2, json: String, repoChecker: (List<Repository>) -> Unit) {
// insert repo
repoDao.insertOrReplace(repo)
// check that the repo got added and retrieved as expected
var repos = repoDao.getRepositories()
assertEquals(1, repos.size)
val repoId = repos[0].repoId
// decode diff from JSON and update DB with it
val diff = j.parseToJsonElement(json).jsonObject
repoDao.updateRepository(repoId, 42, diff)
// fetch repos again and check that the result is as expected
repos = repoDao.getRepositories().sortedBy { it.repoId }
assertEquals(1, repos.size)
assertEquals(repoId, repos[0].repoId)
repoChecker(repos)
}
}

View File

@@ -0,0 +1,120 @@
package org.fdroid.database
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import org.fdroid.index.v2.FeatureV2
import org.fdroid.index.v2.ManifestV2
import org.fdroid.index.v2.MetadataV2
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.index.v2.RepoV2
import org.junit.Assert
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
internal object TestUtils {
fun assertTimestampRecent(timestamp: Long?) {
assertNotNull(timestamp)
assertTrue(System.currentTimeMillis() - timestamp < 2000)
}
fun assertRepoEquals(repoV2: RepoV2, repo: Repository) {
val repoId = repo.repoId
// mirrors
val expectedMirrors = repoV2.mirrors.map { it.toMirror(repoId) }.toSet()
Assert.assertEquals(expectedMirrors, repo.mirrors.toSet())
// anti-features
val expectedAntiFeatures = repoV2.antiFeatures.toRepoAntiFeatures(repoId).toSet()
assertEquals(expectedAntiFeatures, repo.antiFeatures.toSet())
// categories
val expectedCategories = repoV2.categories.toRepoCategories(repoId).toSet()
assertEquals(expectedCategories, repo.categories.toSet())
// release channels
val expectedReleaseChannels = repoV2.releaseChannels.toRepoReleaseChannel(repoId).toSet()
assertEquals(expectedReleaseChannels, repo.releaseChannels.toSet())
// core repo
val coreRepo = repoV2.toCoreRepository(
version = repo.repository.version!!.toLong(),
formatVersion = repo.repository.formatVersion,
certificate = repo.repository.certificate,
).copy(repoId = repoId)
assertEquals(coreRepo, repo.repository)
}
internal fun App.toMetadataV2() = MetadataV2(
added = metadata.added,
lastUpdated = metadata.lastUpdated,
name = metadata.name,
summary = metadata.summary,
description = metadata.description,
webSite = metadata.webSite,
changelog = metadata.changelog,
license = metadata.license,
sourceCode = metadata.sourceCode,
issueTracker = metadata.issueTracker,
translation = metadata.translation,
preferredSigner = metadata.preferredSigner,
video = metadata.video,
authorName = metadata.authorName,
authorEmail = metadata.authorEmail,
authorWebSite = metadata.authorWebSite,
authorPhone = metadata.authorPhone,
donate = metadata.donate ?: emptyList(),
liberapayID = metadata.liberapayID,
liberapay = metadata.liberapay,
openCollective = metadata.openCollective,
bitcoin = metadata.bitcoin,
litecoin = metadata.litecoin,
flattrID = metadata.flattrID,
categories = metadata.categories ?: emptyList(),
icon = icon,
featureGraphic = featureGraphic,
promoGraphic = promoGraphic,
tvBanner = tvBanner,
screenshots = screenshots,
)
fun AppVersion.toPackageVersionV2() = PackageVersionV2(
added = added,
file = file,
src = src,
manifest = ManifestV2(
versionName = manifest.versionName,
versionCode = manifest.versionCode,
usesSdk = manifest.usesSdk,
maxSdkVersion = manifest.maxSdkVersion,
signer = manifest.signer,
usesPermission = usesPermission.sortedBy { it.name },
usesPermissionSdk23 = usesPermissionSdk23.sortedBy { it.name },
nativecode = manifest.nativecode?.sorted() ?: emptyList(),
features = manifest.features?.map { FeatureV2(it) } ?: emptyList(),
),
releaseChannels = releaseChannels,
antiFeatures = version.antiFeatures ?: emptyMap(),
whatsNew = version.whatsNew ?: emptyMap(),
)
fun <T> LiveData<T>.getOrAwaitValue(): T? {
val data = arrayOfNulls<Any>(1)
val latch = CountDownLatch(1)
val observer: Observer<T> = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
removeObserver(this)
}
}
observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
@Suppress("UNCHECKED_CAST")
return data[0] as T?
}
fun <T> LiveData<T>.getOrFail(): T {
return getOrAwaitValue() ?: fail()
}
}

View File

@@ -0,0 +1,234 @@
package org.fdroid.database
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.index.v2.PackageVersionV2
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
import org.fdroid.test.TestRepoUtils.getRandomRepo
import org.fdroid.test.TestUtils.getRandomString
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class VersionTest : DbTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val packageName = getRandomString()
private val packageVersion1 = getRandomPackageVersionV2()
private val packageVersion2 = getRandomPackageVersionV2()
private val packageVersion3 = getRandomPackageVersionV2()
private val versionId1 = packageVersion1.file.sha256
private val versionId2 = packageVersion2.file.sha256
private val versionId3 = packageVersion3.file.sha256
private val isCompatible1 = Random.nextBoolean()
private val isCompatible2 = Random.nextBoolean()
private val packageVersions = mapOf(
versionId1 to packageVersion1,
versionId2 to packageVersion2,
)
private fun getVersion1(repoId: Long) =
packageVersion1.toVersion(repoId, packageName, versionId1, isCompatible1)
private fun getVersion2(repoId: Long) =
packageVersion2.toVersion(repoId, packageName, versionId2, isCompatible2)
private val compatChecker: (PackageVersionV2) -> Boolean = {
when (it.file.sha256) {
versionId1 -> isCompatible1
versionId2 -> isCompatible2
else -> fail()
}
}
@Test
fun insertGetDeleteSingleVersion() {
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1)
val appVersions = versionDao.getAppVersions(repoId, packageName)
assertEquals(1, appVersions.size)
val appVersion = appVersions[0]
assertEquals(versionId1, appVersion.version.versionId)
assertEquals(getVersion1(repoId), appVersion.version)
val manifest = packageVersion1.manifest
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet())
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet())
assertEquals(
manifest.features.map { it.name }.toSet(),
appVersion.version.manifest.features?.toSet()
)
val versionedStrings = versionDao.getVersionedStrings(repoId, packageName)
val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size
assertEquals(expectedSize, versionedStrings.size)
versionDao.deleteAppVersion(repoId, packageName, versionId1)
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
}
@Test
fun insertGetDeleteTwoVersions() {
// insert two versions along with required objects
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1)
versionDao.insert(repoId, packageName, versionId2, packageVersion2, isCompatible2)
// get app versions from DB and assign them correctly
val appVersions = versionDao.getAppVersions(packageName).getOrFail()
assertEquals(2, appVersions.size)
val appVersion = if (versionId1 == appVersions[0].version.versionId) {
appVersions[0]
} else appVersions[1]
val appVersion2 = if (versionId2 == appVersions[0].version.versionId) {
appVersions[0]
} else appVersions[1]
// check first version matches
assertEquals(getVersion1(repoId), appVersion.version)
val manifest = packageVersion1.manifest
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet())
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet())
assertEquals(
manifest.features.map { it.name }.toSet(),
appVersion.version.manifest.features?.toSet()
)
// check second version matches
assertEquals(getVersion2(repoId), appVersion2.version)
val manifest2 = packageVersion2.manifest
assertEquals(manifest2.usesPermission.toSet(), appVersion2.usesPermission.toSet())
assertEquals(manifest2.usesPermissionSdk23.toSet(),
appVersion2.usesPermissionSdk23.toSet())
assertEquals(
manifest.features.map { it.name }.toSet(),
appVersion.version.manifest.features?.toSet()
)
// delete app and check that all associated data also gets deleted
appDao.deleteAppMetadata(repoId, packageName)
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
}
@Test
fun versionsOnlyFromEnabledRepo() {
// insert two versions into the same repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
assertEquals(2, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// add another version into another repo
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId2, packageName, getRandomMetadataV2())
versionDao.insert(repoId2, packageName, versionId3, packageVersion3, true)
assertEquals(3, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(3, versionDao.getVersions(listOf(packageName)).size)
// disable second repo
repoDao.setRepositoryEnabled(repoId2, false)
// now only two versions get returned
assertEquals(2, versionDao.getAppVersions(packageName).getOrFail().size)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
}
@Test
fun versionsSortedByVersionCode() {
// insert three versions into the same repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
versionDao.insert(repoId, packageName, versionId3, packageVersion3, true)
val versions1 = versionDao.getAppVersions(packageName).getOrFail()
val versions2 = versionDao.getVersions(listOf(packageName))
assertEquals(3, versions1.size)
assertEquals(3, versions2.size)
// check that they are sorted as expected
listOf(
packageVersion1.manifest.versionCode,
packageVersion2.manifest.versionCode,
packageVersion3.manifest.versionCode,
).sortedDescending().forEachIndexed { i, versionCode ->
assertEquals(versionCode, versions1[i].version.manifest.versionCode)
assertEquals(versionCode, versions2[i].versionCode)
}
}
@Test
fun getVersionsRespectsAppPrefsIgnore() {
// insert one version into the repo
val repoId = repoDao.insertOrReplace(getRandomRepo())
val versionCode = Random.nextLong(1, Long.MAX_VALUE)
val packageVersion = getRandomPackageVersionV2(versionCode)
val versionId = packageVersion.file.sha256
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, versionId, packageVersion, true)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// default app prefs don't change result
var appPrefs = AppPrefs(packageName)
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// ignore lower version code doesn't change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode - 1)
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
// ignoring exact version code does change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode)
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// ignoring higher version code does change result
appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode + 1)
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// ignoring all updates does change result
appPrefs = appPrefs.toggleIgnoreAllUpdates()
appPrefsDao.update(appPrefs)
assertEquals(0, versionDao.getVersions(listOf(packageName)).size)
// not ignoring all updates brings back version
appPrefs = appPrefs.toggleIgnoreAllUpdates()
appPrefsDao.update(appPrefs)
assertEquals(1, versionDao.getVersions(listOf(packageName)).size)
}
@Test
fun getVersionsConsidersOnlyGivenPackages() {
// insert two versions
val repoId = repoDao.insertOrReplace(getRandomRepo())
appDao.insert(repoId, packageName, getRandomMetadataV2())
versionDao.insert(repoId, packageName, packageVersions, compatChecker)
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// insert versions for a different package
val packageName2 = getRandomString()
appDao.insert(repoId, packageName2, getRandomMetadataV2())
versionDao.insert(repoId, packageName2, packageVersions, compatChecker)
// still only returns above versions
assertEquals(2, versionDao.getVersions(listOf(packageName)).size)
// all versions are returned only if all packages are asked for
assertEquals(4, versionDao.getVersions(listOf(packageName, packageName2)).size)
}
}

View File

@@ -0,0 +1,264 @@
package org.fdroid.index.v1
import android.Manifest
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.fdroid.CompatibilityChecker
import org.fdroid.database.DbTest
import org.fdroid.database.Repository
import org.fdroid.database.TestUtils.getOrAwaitValue
import org.fdroid.database.TestUtils.getOrFail
import org.fdroid.download.Downloader
import org.fdroid.download.DownloaderFactory
import org.fdroid.index.IndexUpdateResult
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.index.v2.ANTI_FEATURE_KNOWN_VULNERABILITY
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class IndexV1UpdaterTest : DbTest() {
@get:Rule
var tmpFolder: TemporaryFolder = TemporaryFolder()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val tempFileProvider: TempFileProvider = mockk()
private val downloaderFactory: DownloaderFactory = mockk()
private val downloader: Downloader = mockk()
private val compatibilityChecker: CompatibilityChecker = CompatibilityChecker { true }
private lateinit var indexUpdater: IndexV1Updater
@Before
override fun createDb() {
super.createDb()
indexUpdater = IndexV1Updater(
database = db,
tempFileProvider = tempFileProvider,
downloaderFactory = downloaderFactory,
compatibilityChecker = compatibilityChecker,
)
}
@Test
fun testIndexV1Processing() {
val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL)
val repo = repoDao.getRepository(repoId) ?: fail()
downloadIndex(repo, TESTY_JAR)
val result = indexUpdater.updateNewRepo(repo, TESTY_FINGERPRINT).noError()
assertIs<IndexUpdateResult.Processed>(result)
// repo got updated
val updatedRepo = repoDao.getRepository(repoId) ?: fail()
assertEquals(TESTY_CERT, updatedRepo.certificate)
assertEquals(TESTY_FINGERPRINT, updatedRepo.fingerprint)
// some assertions ported from old IndexV1UpdaterTest
assertEquals(1, repoDao.getRepositories().size)
assertEquals(63, appDao.countApps())
listOf("fake.app.one", "org.adaway", "This_does_not_exist").forEach { packageName ->
assertNull(appDao.getApp(packageName).getOrAwaitValue())
}
appDao.getAppMetadata().forEach { app ->
val numVersions = versionDao.getVersions(listOf(app.packageName)).size
assertTrue(numVersions > 0)
}
assertEquals(1497639511824, updatedRepo.timestamp)
assertEquals(TESTY_CANONICAL_URL, updatedRepo.address)
assertEquals("non-public test repo", updatedRepo.repository.name.values.first())
assertEquals(18, updatedRepo.version)
assertEquals("/icons/fdroid-icon.png", updatedRepo.repository.icon?.values?.first()?.name)
val description = "This is a repository of apps to be used with F-Droid. " +
"Applications in this repository are either official binaries built " +
"by the original application developers, or are binaries built " +
"from source by the admin of f-droid.org using the tools on " +
"https://gitlab.com/u/fdroid. "
assertEquals(description, updatedRepo.repository.description.values.first())
assertEquals(
setOf(TESTY_CANONICAL_URL, "http://frkcchxlcvnb4m5a.onion/fdroid/repo"),
updatedRepo.mirrors.map { it.url }.toSet(),
)
// Make sure the per-apk anti features which are new in index v1 get added correctly.
val wazeVersion = versionDao.getVersions(listOf("com.waze")).find {
it.manifest.versionCode == 1019841L
}
assertNotNull(wazeVersion)
assertEquals(setOf(ANTI_FEATURE_KNOWN_VULNERABILITY), wazeVersion.antiFeatures?.keys)
val protoVersion = versionDao.getAppVersions("io.proto.player").getOrFail().find {
it.version.versionCode == 1110L
}
assertNotNull(protoVersion)
assertEquals("/io.proto.player-1.apk", protoVersion.version.file.name)
val perms = protoVersion.usesPermission.map { it.name }
assertTrue(perms.contains(Manifest.permission.READ_EXTERNAL_STORAGE))
assertTrue(perms.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE))
assertFalse(perms.contains(Manifest.permission.READ_CALENDAR))
val icon = appDao.getApp("com.autonavi.minimap").getOrFail()?.icon?.values?.first()?.name
assertEquals("/com.autonavi.minimap/en-US/icon.png", icon)
// update again and get unchanged
downloadIndex(updatedRepo, TESTY_JAR)
val result2 = indexUpdater.update(updatedRepo).noError()
assertIs<IndexUpdateResult.Unchanged>(result2)
}
@Test
fun testIndexV1WithWrongCert() {
val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL)
val repo = repoDao.getRepository(repoId) ?: fail()
downloadIndex(repo, TESTY_JAR)
val result = indexUpdater.updateNewRepo(repo, "not the right fingerprint")
assertIs<IndexUpdateResult.Error>(result)
assertIs<SigningException>(result.e)
}
@Test
fun testIndexV1WithOldTimestamp() {
val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL)
val repo = repoDao.getRepository(repoId) ?: fail()
val futureRepo =
repo.copy(repository = repo.repository.copy(timestamp = System.currentTimeMillis()))
downloadIndex(futureRepo, TESTY_JAR)
val result = indexUpdater.updateNewRepo(futureRepo, TESTY_FINGERPRINT)
assertIs<IndexUpdateResult.Error>(result)
assertIs<OldIndexException>(result.e)
assertFalse((result.e as OldIndexException).isSameTimestamp)
}
@Test
fun testIndexV1WithCorruptAppPackageName() {
val result = testBadTestyJar("testy.at.or.at_corrupt_app_package_name_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
}
@Test
fun testIndexV1WithCorruptPackageName() {
val result = testBadTestyJar("testy.at.or.at_corrupt_package_name_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
}
@Test
fun testIndexV1WithBadTestyJarNoManifest() {
val result = testBadTestyJar("testy.at.or.at_no-MANIFEST.MF_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
assertIs<SigningException>(result.e)
}
@Test
fun testIndexV1WithBadTestyJarNoSigningCert() {
val result = testBadTestyJar("testy.at.or.at_no-.RSA_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
}
@Test
fun testIndexV1WithBadTestyJarNoSignature() {
val result = testBadTestyJar("testy.at.or.at_no-.SF_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
}
@Test
fun testIndexV1WithBadTestyJarNoSignatureFiles() {
val result = testBadTestyJar("testy.at.or.at_no-signature_index-v1.jar")
assertIs<IndexUpdateResult.Error>(result)
assertIs<SigningException>(result.e)
}
@Suppress("DEPRECATION")
private fun downloadIndex(repo: Repository, jar: String) {
val uri = Uri.parse("${repo.address}/index-v1.jar")
val jarFile = tmpFolder.newFile()
assets.open(jar).use { inputStream ->
jarFile.outputStream().use { inputStream.copyTo(it) }
}
every { tempFileProvider.createTempFile() } returns jarFile
every {
downloaderFactory.createWithTryFirstMirror(repo, uri, jarFile)
} returns downloader
every { downloader.cacheTag = null } just Runs
every { downloader.download() } just Runs
every { downloader.hasChanged() } returns true
every { downloader.cacheTag } returns null
}
private fun testBadTestyJar(jar: String): IndexUpdateResult {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = repoDao.getRepository(repoId) ?: fail()
downloadIndex(repo, jar)
return indexUpdater.updateNewRepo(repo, null)
}
/**
* Easier for debugging, if we throw the index error.
*/
private fun IndexUpdateResult.noError(): IndexUpdateResult {
if (this is IndexUpdateResult.Error) throw e
return this
}
}
private const val TESTY_CANONICAL_URL = "http://testy.at.or.at/fdroid/repo"
private const val TESTY_JAR = "testy.at.or.at_index-v1.jar"
private const val TESTY_FINGERPRINT =
"818e469465f96b704e27be2fee4c63ab9f83ddf30e7a34c7371a4728d83b0bc1"
private const val TESTY_CERT = "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b" +
"050030213110300e060355040b1307462d44726f6964310d300b060355040313" +
"04736f7661301e170d3136303832333133333131365a170d3434303130393133" +
"333131365a30213110300e060355040b1307462d44726f6964310d300b060355" +
"04031304736f766130820222300d06092a864886f70d01010105000382020f00" +
"3082020a0282020100dfdcd120f3ab224999dddf4ea33ea588d295e4d7130bef" +
"48c143e9d76e5c0e0e9e5d45e64208e35feebc79a83f08939dd6a343b7d1e217" +
"9930a105a1249ccd36d88ff3feffc6e4dc53dae0163a7876dd45ecc1ddb0adf5" +
"099aa56c1a84b52affcd45d0711ffa4de864f35ac0333ebe61ea8673eeda35a8" +
"8f6af678cc4d0f80b089338ac8f2a8279a64195c611d19445cab3fd1a020afed" +
"9bd739bb95142fb2c00a8f847db5ef3325c814f8eb741bacf86ed3907bfe6e45" +
"64d2de5895df0c263824e0b75407589bae2d3a4666c13b92102d8781a8ee9bb4" +
"a5a1a78c4a9c21efdaf5584da42e84418b28f5a81d0456a3dc5b420991801e6b" +
"21e38c99bbe018a5b2d690894a114bc860d35601416aa4dc52216aff8a288d47" +
"75cddf8b72d45fd2f87303a8e9c0d67e442530be28eaf139894337266e0b33d5" +
"7f949256ab32083bcc545bc18a83c9ab8247c12aea037e2b68dee31c734cb1f0" +
"4f241d3b94caa3a2b258ffaf8e6eae9fbbe029a934dc0a0859c5f12033481269" +
"3a1c09352340a39f2a678dbc1afa2a978bfee43afefcb7e224a58af2f3d647e5" +
"745db59061236b8af6fcfd93b3602f9e456978534f3a7851e800071bf56da804" +
"01c81d91c45f82568373af0576b1cc5eef9b85654124b6319770be3cdba3fbeb" +
"e3715e8918fb6c8966624f3d0e815effac3d2ee06dd34ab9c693218b2c7c06ba" +
"99d6b74d4f17b8c3cb0203010001a321301f301d0603551d0e04160414d62bee" +
"9f3798509546acc62eb1de14b08b954d4f300d06092a864886f70d01010b0500" +
"0382020100743f7c5692085895f9d1fffad390fb4202c15f123ed094df259185" +
"960fd6dadf66cb19851070f180297bba4e6996a4434616573b375cfee94fee73" +
"a4505a7ec29136b7e6c22e6436290e3686fe4379d4e3140ec6a08e70cfd3ed5b" +
"634a5eb5136efaaabf5f38e0432d3d79568a556970b8cfba2972f5d23a3856d8" +
"a981b9e9bbbbb88f35e708bde9cbc5f681cbd974085b9da28911296fe2579fa6" +
"4bbe9fa0b93475a7a8db051080b0c5fade0d1c018e7858cd4cbe95145b0620e2" +
"f632cbe0f8af9cbf22e2fdaa72245ae31b0877b07181cc69dd2df74454251d8d" +
"e58d25e76354abe7eb690f22e59b08795a8f2c98c578e0599503d90859276340" +
"72c82c9f82abd50fd12b8fd1a9d1954eb5cc0b4cfb5796b5aaec0356643b4a65" +
"a368442d92ef94edd3ac6a2b7fe3571b8cf9f462729228aab023ef9183f73792" +
"f5379633ccac51079177d604c6bc1873ada6f07d8da6d68c897e88a5fa5d63fd" +
"b8df820f46090e0716e7562dd3c140ba279a65b996f60addb0abe29d4bf2f5ab" +
"e89480771d492307b926d91f02f341b2148502903c43d40f3c6c86a811d06071" +
"1f0698b384acdcc0add44eb54e42962d3d041accc715afd49407715adc09350c" +
"b55e8d9281a3b0b6b5fcd91726eede9b7c8b13afdebb2c2b377629595f1096ba" +
"62fb14946dbac5f3c5f0b4e5b712e7acc7dcf6c46cdc5e6d6dfdeee55a0c92c2" +
"d70f080ac6"

View File

@@ -0,0 +1,294 @@
package org.fdroid.index.v2
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.fdroid.CompatibilityChecker
import org.fdroid.database.DbTest
import org.fdroid.database.Repository
import org.fdroid.database.TestUtils.assertTimestampRecent
import org.fdroid.download.Downloader
import org.fdroid.download.DownloaderFactory
import org.fdroid.index.IndexFormatVersion.TWO
import org.fdroid.index.IndexUpdateResult
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.test.TestDataEntryV2
import org.fdroid.test.TestDataMaxV2
import org.fdroid.test.TestDataMidV2
import org.fdroid.test.TestDataMinV2
import org.fdroid.test.VerifierConstants.CERTIFICATE
import org.fdroid.test.VerifierConstants.FINGERPRINT
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class IndexV2UpdaterTest : DbTest() {
@get:Rule
var tmpFolder: TemporaryFolder = TemporaryFolder()
private val tempFileProvider: TempFileProvider = mockk()
private val downloaderFactory: DownloaderFactory = mockk()
private val downloader: Downloader = mockk()
private val compatibilityChecker: CompatibilityChecker = CompatibilityChecker { true }
private lateinit var indexUpdater: IndexV2Updater
@Before
override fun createDb() {
super.createDb()
indexUpdater = IndexV2Updater(
database = db,
tempFileProvider = tempFileProvider,
downloaderFactory = downloaderFactory,
compatibilityChecker = compatibilityChecker,
)
}
@Test
fun testFullIndexEmptyToMin() {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "index-min-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMin.index
)
val result = indexUpdater.updateNewRepo(repo, FINGERPRINT).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMinV2.index)
// check that certificate and format version got entered
val updatedRepo = repoDao.getRepository(repoId) ?: fail()
assertEquals(TWO, updatedRepo.formatVersion)
assertEquals(CERTIFICATE, updatedRepo.certificate)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testFullIndexEmptyToMid() {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-mid/entry.jar",
jsonPath = "index-mid-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMid.index
)
val result = indexUpdater.updateNewRepo(repo, FINGERPRINT).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMidV2.index)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testFullIndexEmptyToMax() {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-max/entry.jar",
jsonPath = "index-max-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMax.index
)
val result = indexUpdater.updateNewRepo(repo, FINGERPRINT).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMaxV2.index)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testDiffMinToMid() {
val repoId = streamIndexV2IntoDb("index-min-v2.json")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-mid/entry.jar",
jsonPath = "diff-empty-mid/42.json",
entryFileV2 = TestDataEntryV2.emptyToMid.diffs["42"] ?: fail()
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMidV2.index)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testDiffEmptyToMin() {
val repoId = streamIndexV2IntoDb("index-empty-v2.json")
repoDao.updateRepository(repoId, CERTIFICATE)
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "diff-empty-min/23.json",
entryFileV2 = TestDataEntryV2.emptyToMin.diffs["23"] ?: fail()
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMinV2.index)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testDiffMidToMax() {
val repoId = streamIndexV2IntoDb("index-mid-v2.json")
repoDao.updateRepository(repoId, CERTIFICATE)
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-max/entry.jar",
jsonPath = "diff-empty-max/1337.json",
entryFileV2 = TestDataEntryV2.emptyToMax.diffs["1337"] ?: fail()
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMaxV2.index)
assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testSameTimestampUnchanged() {
val repoId = streamIndexV2IntoDb("index-min-v2.json")
repoDao.updateRepository(repoId, CERTIFICATE)
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "diff-empty-min/23.json",
entryFileV2 = TestDataEntryV2.emptyToMin.diffs["23"] ?: fail()
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Unchanged, result)
assertDbEquals(repoId, TestDataMinV2.index)
assertNull(repoDao.getRepository(repoId)?.lastUpdated)
}
@Test
fun testHigherTimestampUnchanged() {
val repoId = streamIndexV2IntoDb("index-mid-v2.json")
repoDao.updateRepository(repoId, CERTIFICATE)
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "diff-empty-min/23.json",
entryFileV2 = TestDataEntryV2.emptyToMin.diffs["23"] ?: fail()
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Unchanged, result)
assertDbEquals(repoId, TestDataMidV2.index)
}
@Test
fun testNoDiffFoundIndexFallback() {
val repoId = streamIndexV2IntoDb("index-empty-v2.json")
repoDao.updateRepository(repoId, CERTIFICATE)
// fake timestamp of internal repo, so we will fail to find a diff in entry.json
val newRepo = repoDao.getRepository(repoId)?.repository?.copy(timestamp = 22) ?: fail()
repoDao.updateRepository(newRepo)
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "index-min-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMin.index
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMinV2.index)
}
@Test
fun testWrongFingerprint() {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "index-min-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMin.index
)
val result = indexUpdater.updateNewRepo(repo, "wrong fingerprint")
assertTrue(result is IndexUpdateResult.Error)
assertTrue(result.e is SigningException)
}
@Test
fun testNormalUpdateOnRepoWithMissingFingerprint() {
val repoId = repoDao.insertEmptyRepo("http://example.org")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-min/entry.jar",
jsonPath = "index-min-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMin.index
)
val result = indexUpdater.update(repo)
assertTrue(result is IndexUpdateResult.Error)
assertTrue(result.e is IllegalArgumentException)
}
/**
* Ensures that a v1 repo can't use a diff when upgrading to v1,
* but must use a full index update.
*/
@Test
fun testV1ToV2ForcesFullUpdateEvenIfDiffExists() {
val repoId = streamIndexV1IntoDb("index-min-v1.json")
val repo = prepareUpdate(
repoId = repoId,
entryPath = "diff-empty-mid/entry.jar",
jsonPath = "index-mid-v2.json",
entryFileV2 = TestDataEntryV2.emptyToMid.index,
)
val result = indexUpdater.update(repo).noError()
assertEquals(IndexUpdateResult.Processed, result)
assertDbEquals(repoId, TestDataMidV2.index)
// check that format version got upgraded
val updatedRepo = repoDao.getRepository(repoId) ?: fail()
assertEquals(TWO, updatedRepo.formatVersion)
}
private fun prepareUpdate(
repoId: Long,
entryPath: String,
jsonPath: String,
entryFileV2: EntryFileV2,
): Repository {
val entryFile = tmpFolder.newFile()
val indexFile = tmpFolder.newFile()
val repo = repoDao.getRepository(repoId) ?: fail()
val entryUri = Uri.parse("${repo.address}/entry.jar")
val indexUri = Uri.parse("${repo.address}/${entryFileV2.name.trimStart('/')}")
assets.open(entryPath).use { inputStream ->
entryFile.outputStream().use { inputStream.copyTo(it) }
}
assets.open(jsonPath).use { inputStream ->
indexFile.outputStream().use { inputStream.copyTo(it) }
}
every { tempFileProvider.createTempFile() } returnsMany listOf(entryFile, indexFile)
every {
downloaderFactory.createWithTryFirstMirror(repo, entryUri, entryFile)
} returns downloader
every { downloader.download(-1) } just Runs
every {
downloaderFactory.createWithTryFirstMirror(repo, indexUri, indexFile)
} returns downloader
every { downloader.download(entryFileV2.size, entryFileV2.sha256) } just Runs
return repo
}
/**
* Easier for debugging, if we throw the index error.
*/
private fun IndexUpdateResult.noError(): IndexUpdateResult {
if (this is IndexUpdateResult.Error) throw e
return this
}
}