From 2d7e94c7a5a7732f783de30e00bbfd9b9dfbaf8c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 2 Mar 2026 15:13:01 -0300 Subject: [PATCH] Migrate to AGP9 fixes multi-platform JVM builds --- .gitlab-ci.yml | 10 +- app/build.gradle.kts | 1 - .../java/org/fdroid/download/DnsCacheTest.kt | 2 +- .../org/fdroid/history/HistoryManagerTest.kt | 2 +- build.gradle | 1 - gradle/libs.versions.toml | 11 +- gradle/verification-metadata.xml | 19815 +--------------- gradle/wrapper/gradle-wrapper.properties | 4 +- legacy/build.gradle | 30 +- .../org/fdroid/fdroid/net/DnsCacheTest.kt | 6 +- .../fdroid/fdroid/work/AppUpdateWorkerTest.kt | 10 +- libs/core/api/jvm/core.api | 8 + libs/core/build.gradle.kts | 2 +- libs/database/build.gradle.kts | 12 +- .../java/org/fdroid/database/AppDaoTest.kt | 340 +- .../org/fdroid/database/AppListItemsTest.kt | 1495 +- .../fdroid/database/AppOverviewItemsTest.kt | 839 +- .../org/fdroid/database/AppPrefsDaoTest.kt | 263 +- .../java/org/fdroid/database/AppTest.kt | 48 +- .../database/CountryCodeMigrationTest.kt | 155 +- .../dbTest/java/org/fdroid/database/DbTest.kt | 205 +- .../fdroid/database/DbUpdateCheckerTest.kt | 575 +- .../org/fdroid/database/FDroidDatabaseTest.kt | 51 +- .../database/FtsAddColumnsMigrationTest.kt | 385 +- .../FtsCaseInsensitiveMigrationTest.kt | 307 +- .../org/fdroid/database/IndexV1InsertTest.kt | 189 +- .../org/fdroid/database/IndexV2DiffTest.kt | 731 +- .../org/fdroid/database/IndexV2InsertTest.kt | 112 +- .../fdroid/database/MultiRepoMigrationTest.kt | 618 +- .../database/PreferredRepoMigrationTest.kt | 150 +- .../database/RepoCertNonNullMigrationTest.kt | 135 +- .../org/fdroid/database/RepositoryDaoTest.kt | 702 +- .../org/fdroid/database/RepositoryDiffTest.kt | 651 +- .../java/org/fdroid/database/TestUtils.kt | 201 +- .../java/org/fdroid/database/VersionTest.kt | 425 +- .../java/org/fdroid/index/IndexUpdaterTest.kt | 34 +- .../org/fdroid/index/v1/IndexV1UpdaterTest.kt | 360 +- .../org/fdroid/index/v2/IndexV2UpdaterTest.kt | 558 +- .../main/java/org/fdroid/repo/RepoAdder.kt | 2 +- .../org/fdroid/database/ConvertersTest.kt | 2 +- .../java/org/fdroid/repo/RepoAdderTest.kt | 19 +- libs/download/api/jvm/download.api | 137 + libs/download/build.gradle.kts | 6 +- .../kotlin/org/fdroid/download/HttpManager.kt | 5 + .../kotlin/org/fdroid/download/HttpManager.kt | 2 - libs/index/api/jvm/index.api | 1119 + libs/index/build.gradle.kts | 3 +- .../index/v1/IndexV1StreamProcessorTest.kt | 2 +- .../v2/IndexV2FullStreamProcessorTest.kt | 2 +- .../fdroid/index/v2/ReflectionDifferTest.kt | 2 +- .../org/fdroid/index/IndexConverterTest.kt | 10 +- .../kotlin/org/fdroid/index/v1/IndexV1Test.kt | 13 +- .../kotlin/org/fdroid/index/v2/EntryTest.kt | 9 +- .../v2/IndexV2DiffStreamProcessorTest.kt | 2 +- .../kotlin/org/fdroid/index/v2/IndexV2Test.kt | 11 +- libs/sharedTest/build.gradle.kts | 41 +- .../{main => commonMain}/AndroidManifest.xml | 0 .../kotlin/org/fdroid/test/DiffUtils.kt | 0 .../kotlin/org/fdroid/test/TestAppUtils.kt | 0 .../kotlin/org/fdroid/test/TestDataEntry.kt | 0 .../kotlin/org/fdroid/test/TestDataV1.kt | 0 .../kotlin/org/fdroid/test/TestDataV2.kt | 0 .../kotlin/org/fdroid/test/TestRepoUtils.kt | 0 .../kotlin/org/fdroid/test/TestUtils.kt | 8 + .../org/fdroid/test/TestVersionUtils.kt | 0 .../org/fdroid/test/VerifierConstants.kt | 0 .../resources}/diff-empty-max/1337.json | 0 .../resources}/diff-empty-max/23.json | 0 .../resources}/diff-empty-max/42.json | 0 .../resources}/diff-empty-max/entry.jar | Bin .../resources}/diff-empty-max/entry.json | 0 .../resources}/diff-empty-mid/23.json | 0 .../resources}/diff-empty-mid/42.json | 0 .../resources}/diff-empty-mid/entry.jar | Bin .../resources}/diff-empty-mid/entry.json | 0 .../resources}/diff-empty-min/23.json | 0 .../resources}/diff-empty-min/entry.jar | Bin .../resources}/diff-empty-min/entry.json | 0 .../resources}/entry-empty-v2.json | 0 .../resources}/guardianproject_entry.jar | Bin .../resources}/guardianproject_index-v1.json | 0 .../resources}/index-empty-v1.json | 0 .../resources}/index-empty-v2.json | 0 .../resources}/index-max-v1.json | 0 .../resources}/index-max-v2.json | 0 .../resources}/index-mid-v1.json | 0 .../resources}/index-mid-v2.json | 0 .../resources}/index-min-reordered-v2.json | 0 .../resources}/index-min-v1.json | 0 .../resources}/index-min-v2.json | 0 .../resources}/localized.json | 0 ...r.at_corrupt_app_package_name_index-v1.jar | Bin ...at.or.at_corrupt_package_name_index-v1.jar | Bin .../resources}/testy.at.or.at_index-v1.jar | Bin .../testy.at.or.at_no-.RSA_index-v1.jar | Bin .../testy.at.or.at_no-.SF_index-v1.jar | Bin ...testy.at.or.at_no-MANIFEST.MF_index-v1.jar | Bin .../testy.at.or.at_no-signature_index-v1.jar | Bin 98 files changed, 6818 insertions(+), 24020 deletions(-) create mode 100644 libs/core/api/jvm/core.api create mode 100644 libs/download/api/jvm/download.api create mode 100644 libs/index/api/jvm/index.api rename libs/sharedTest/src/{main => commonMain}/AndroidManifest.xml (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/DiffUtils.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestAppUtils.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestDataEntry.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestDataV1.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestDataV2.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestRepoUtils.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestUtils.kt (88%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/TestVersionUtils.kt (100%) rename libs/sharedTest/src/{main => commonMain}/kotlin/org/fdroid/test/VerifierConstants.kt (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-max/1337.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-max/23.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-max/42.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-max/entry.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-max/entry.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-mid/23.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-mid/42.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-mid/entry.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-mid/entry.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-min/23.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-min/entry.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/diff-empty-min/entry.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/entry-empty-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/guardianproject_entry.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/guardianproject_index-v1.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-empty-v1.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-empty-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-max-v1.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-max-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-mid-v1.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-mid-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-min-reordered-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-min-v1.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/index-min-v2.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/localized.json (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_corrupt_app_package_name_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_corrupt_package_name_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_no-.RSA_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_no-.SF_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_no-MANIFEST.MF_index-v1.jar (100%) rename libs/sharedTest/src/{main/assets => commonMain/resources}/testy.at.or.at_no-signature_index-v1.jar (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6d4909716..1773c654a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -129,7 +129,7 @@ libs download test: - changes: - libs/download/**/* script: - - ./gradlew :libs:download:testDebugUnitTest + - ./gradlew :libs:download:testAndroidHostTest libs index test: <<: *test-template @@ -138,7 +138,7 @@ libs index test: - changes: - libs/index/**/* script: - - ./gradlew :libs:index:testDebugUnitTest + - ./gradlew :libs:index:testAndroidHostTest app lint: <<: *test-template @@ -176,7 +176,7 @@ libs lint: - changes: - libs/**/* script: - - ./gradlew :libs:core:lint :libs:database:lint :libs:download:lint :libs:index:lint :libs:sharedTest:lint + - ./gradlew :libs:core:lint :libs:database:lint :libs:download:lint :libs:index:lint - ./gradlew :libs:core:ktfmtCheck :libs:database:ktfmtCheck :libs:download:ktfmtCheck :libs:index:ktfmtCheck :libs:sharedTest:ktfmtCheck - ./gradlew checkLegacyAbi @@ -332,9 +332,9 @@ libs database schema: - ./gradlew :app:installBasicDefaultDebug :legacy:installFullDebug - adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity - export FLAG="-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest,androidx.test.filters.FlakyTest" - - ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedCheck :libs:index:connectedCheck + - ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest - export FLAG="-Pandroid.testInstrumentationRunnerArguments.annotation=androidx.test.filters.FlakyTest" - - for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedCheck :libs:index:connectedCheck && break; done || exit 137 + - for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest && break; done || exit 137 allow_failure: exit_codes: 137 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3acafff00..596785d1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,7 +4,6 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.android.ksp) alias(libs.plugins.android.hilt) - alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.plugin.serialization) alias(libs.plugins.jetbrains.compose.compiler) alias(libs.plugins.screenshot) diff --git a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt index e0e9973f1..137e396b1 100644 --- a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt +++ b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt @@ -4,12 +4,12 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import java.net.InetAddress -import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import org.fdroid.settings.SettingsManager +import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) diff --git a/app/src/test/java/org/fdroid/history/HistoryManagerTest.kt b/app/src/test/java/org/fdroid/history/HistoryManagerTest.kt index eb9bb591d..9c1911bac 100644 --- a/app/src/test/java/org/fdroid/history/HistoryManagerTest.kt +++ b/app/src/test/java/org/fdroid/history/HistoryManagerTest.kt @@ -8,10 +8,10 @@ import io.mockk.mockk import io.mockk.verify import java.io.FileOutputStream import kotlin.random.Random -import kotlin.test.Test import kotlin.test.assertEquals import org.fdroid.settings.SettingsManager import org.junit.Rule +import org.junit.Test import org.junit.rules.TemporaryFolder private const val MAX_EVENTS = 10 diff --git a/build.gradle b/build.gradle index 2342cd566..6b1486003 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,6 @@ plugins { alias libs.plugins.android.library apply false alias libs.plugins.android.ksp apply false alias libs.plugins.android.hilt apply false - alias libs.plugins.jetbrains.kotlin.android apply false alias libs.plugins.jetbrains.kotlin.multiplatform apply false alias libs.plugins.jetbrains.kotlin.plugin.serialization apply false alias libs.plugins.jetbrains.compose.compiler apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5a1f6d24..6a0c3bd59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] compileSdk = "36" kotlin = "2.3.10" -androidGradlePlugin = "8.13.2" +androidGradlePlugin = "9.1.0" androidKspPlugin = "2.3.4" -hilt = "2.57.2" +hilt = "2.59.2" hiltWork = "1.3.0" hiltNavigationCompose = "1.3.0" dokka = "2.1.0" @@ -20,7 +20,7 @@ okhttp = "4.12.0" room = "2.8.4" glide = "5.0.5" glideCompose = "1.0.0-beta08" -coilCompose = "3.3.0" +coilCompose = "3.4.0" molecule = "2.2.0" hints = "2.0.1" composePreference = "2.1.1" @@ -38,7 +38,7 @@ androidxConstraintlayout = "2.2.1" androidxSwipeRefreshLayout = "1.2.0" androidxVectordrawable = "1.2.0" androidxGridlayout = "1.1.0" -androidxComposeBom = "2026.02.00" +androidxComposeBom = "2026.02.01" androidxActivityCompose = "1.12.4" accompanistDrawablepainter = "0.37.3" @@ -85,7 +85,7 @@ androidxTestUiautomator = "2.3.0" androidxTestMonitor = "1.8.0" mockitoCore = "5.1.1" hamcrest = "2.2" -goncalossilvaResources = "0.2.1" +goncalossilvaResources = "0.15.0" turbine = "1.2.1" json = "20220320" @@ -209,7 +209,6 @@ android-multiplatform-library = { id = "com.android.kotlin.multiplatform.library android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-ksp = { id = "com.google.devtools.ksp", version.ref = "androidKspPlugin" } android-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e0123177f..31c596ba0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -183,257 +183,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -449,26 +223,11 @@ - - - - - - - - - - - - - - - @@ -479,154 +238,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -634,46 +270,12 @@ - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -681,609 +283,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1291,54 +323,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -1346,1354 +333,119 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -2716,101 +468,19 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -2823,61 +493,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2888,26 +503,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -2915,18 +510,12 @@ - - - - + - - - - + @@ -2939,138 +528,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - + @@ -3080,10 +560,7 @@ - - - - + @@ -3093,7 +570,7 @@ - + @@ -3101,82 +578,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -3189,11 +608,6 @@ - - - - - @@ -3204,22 +618,14 @@ - - - - - - - - - - - + + + @@ -3259,85 +665,17 @@ - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -3345,1048 +683,104 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + @@ -4394,14 +788,6 @@ - - - - - - - - @@ -4417,72 +803,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -4495,10 +835,7 @@ - - - - + @@ -4506,42 +843,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -4549,743 +858,86 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5296,348 +948,91 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5645,7 +1040,7 @@ - + @@ -5663,19 +1058,6 @@ - - - - - - - - - - - - - @@ -5688,10 +1070,7 @@ - - - - + @@ -5701,39 +1080,22 @@ - - - - - - - - - - - - + - + - - - - + - - - - + @@ -5741,11 +1103,6 @@ - - - - - @@ -5756,533 +1113,79 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -6290,1590 +1193,269 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - - - - - - - - - - - - + - + @@ -7886,291 +1468,54 @@ - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -8178,67 +1523,14 @@ - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -8246,354 +1538,64 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -8606,978 +1608,194 @@ - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -9585,462 +1803,114 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + @@ -10048,90 +1918,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10144,92 +1940,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10237,75 +1965,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10322,47 +1993,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -10372,7 +2010,6 @@ - @@ -10381,84 +2018,49 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -10466,55 +2068,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -10523,99 +2083,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10626,25 +2103,11 @@ - - - - - - - - - - - - - - @@ -10660,37 +2123,16 @@ - - - - - - - - - - - - - - - - - - - - - @@ -10706,69 +2148,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10777,18 +2163,15 @@ - - - + + + - - - @@ -10800,68 +2183,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10872,14 +2198,6 @@ - - - - - - - - @@ -10895,79 +2213,23 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10975,22 +2237,9 @@ - - - - - - - - - - - - - @@ -11013,43 +2262,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11061,27 +2273,9 @@ - - - - - - - - - - - - - - - - - - - - - + + + @@ -11099,12 +2293,9 @@ - - - - - - + + + @@ -11112,24 +2303,9 @@ - - - - - - - - - - - - - - - - - - + + + @@ -11137,19 +2313,9 @@ - - - - - - - - - - - - - + + + @@ -11157,19 +2323,9 @@ - - - - - - - - - - - - - + + + @@ -11177,19 +2333,9 @@ - - - - - - - - - - - - - + + + @@ -11197,24 +2343,9 @@ - - - - - - - - - - - - - - - - - - + + + @@ -11222,19 +2353,9 @@ - - - - - - - - - - - - - + + + @@ -11242,111 +2363,31 @@ + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11354,13 +2395,11 @@ - - + - @@ -11369,12 +2408,6 @@ - - - - - - @@ -11385,249 +2418,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -11637,78 +2435,34 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11724,106 +2478,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11839,144 +2503,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11987,60 +2543,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + @@ -12048,76 +2568,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -12128,39 +2592,25 @@ - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + @@ -12173,15 +2623,9 @@ - - - - - - - - - + + + @@ -12194,59 +2638,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -12257,26 +2658,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12287,26 +2668,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12322,26 +2683,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12352,26 +2693,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12382,26 +2703,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12417,26 +2718,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -12452,213 +2733,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -12666,1520 +2748,89 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -14187,1023 +2838,106 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -15214,21 +2948,6 @@ - - - - - - - - - - - - - - - @@ -15239,21 +2958,6 @@ - - - - - - - - - - - - - - - @@ -15264,21 +2968,6 @@ - - - - - - - - - - - - - - - @@ -15289,21 +2978,6 @@ - - - - - - - - - - - - - - - @@ -15314,21 +2988,6 @@ - - - - - - - - - - - - - - - @@ -15339,21 +2998,6 @@ - - - - - - - - - - - - - - - @@ -15364,21 +3008,6 @@ - - - - - - - - - - - - - - - @@ -15389,21 +3018,6 @@ - - - - - - - - - - - - - - - @@ -15414,51 +3028,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -15474,41 +3053,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -15519,73 +3073,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -15596,262 +3095,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -15859,40 +3150,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -15905,201 +3168,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -16107,65 +3208,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16175,47 +3217,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16226,9 +3227,6 @@ - - - @@ -16245,150 +3243,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16399,57 +3253,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16460,72 +3268,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16541,31 +3288,11 @@ - - - - - - - - - - - - - - - - - - - - @@ -16583,38 +3310,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16650,201 +3348,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16855,74 +3368,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16931,49 +3378,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -16981,101 +3395,42 @@ - - - - - - - - - - - - - + + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -17085,60 +3440,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -17154,19 +3473,9 @@ - - - - - - - - - - - - - + + + @@ -17174,1643 +3483,156 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -18821,284 +3643,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -19106,687 +3683,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -19808,181 +3779,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -19998,116 +3809,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -20123,126 +3839,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -20251,545 +3862,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -20800,72 +3917,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -20876,57 +3927,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -20947,359 +3952,61 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -21310,246 +4017,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -21565,51 +4062,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -21620,16 +4087,6 @@ - - - - - - - - - - @@ -21640,71 +4097,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -21712,208 +4124,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -21921,47 +4167,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -21969,65 +4177,24 @@ + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -22035,12 +4202,6 @@ - - - - - - @@ -22051,776 +4212,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -22831,82 +4297,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -22928,27 +4328,10 @@ - - - - - - - - - - - - - - - - - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6048b9914..7abf69a2f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Tue Oct 22 15:45:27 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip +distributionSha256Sum=17f277867f6914d61b1aa02efab1ba7bb439ad652ca485cd8ca6842fccec6e43 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/legacy/build.gradle b/legacy/build.gradle index 46fdd0a4c..7d6d87c97 100644 --- a/legacy/build.gradle +++ b/legacy/build.gradle @@ -1,6 +1,8 @@ +import com.android.build.api.variant.ResValue +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id 'com.android.application' - id 'kotlin-android' id 'org.jetbrains.kotlin.plugin.compose' } @@ -52,10 +54,11 @@ android { minifyEnabled true shrinkResources true buildConfigField "String", "PRIVILEGED_EXTENSION_PACKAGE_NAME", privilegedExtensionApplicationId - buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"' // String needs both quotes + buildConfigField "String", "ACRA_REPORT_EMAIL", '"reports@f-droid.org"' + // String needs both quotes buildConfigField "String", "ACRA_REPORT_FILE_NAME", '"ACRA-report.stacktrace.json"' - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testProguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro' } debug { getIsDefault().set(true) @@ -63,7 +66,7 @@ android { applicationIdSuffix ".debug" versionNameSuffix "-debug" // testProguardFiles gets partially ignored for instrumentation tests - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'src/androidTest/proguard-rules.pro' println 'buildTypes.debug defaultConfig.versionCode ' + defaultConfig.versionCode } } @@ -81,8 +84,14 @@ android { applicationIdSuffix ".basic" } } - applicationVariants.configureEach { variant -> - variant.resValue "string", "applicationId", variant.applicationId + + androidComponents { + onVariants(selector().all()) { variant -> + variant.resValues.put( + variant.makeResValueKey("string", "applicationId"), + new ResValue(variant.applicationId.get(), null) + ) + } } compileOptions { @@ -91,8 +100,10 @@ android { targetCompatibility JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = '17' + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } } aaptOptions { @@ -101,6 +112,7 @@ android { buildFeatures { buildConfig true + resValues = true compose true aidl true } diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt b/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt index 56e8d9891..22c2e3bbd 100644 --- a/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt +++ b/legacy/src/androidTest/java/org/fdroid/fdroid/net/DnsCacheTest.kt @@ -1,10 +1,10 @@ package org.fdroid.fdroid.net -import org.fdroid.fdroid.Preferences -import org.junit.Assert import java.net.InetAddress import java.util.Arrays -import kotlin.test.Test +import org.fdroid.fdroid.Preferences +import org.junit.Assert +import org.junit.Test class DnsCacheTest { diff --git a/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt b/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt index 6e46e2ad8..7667b7a79 100644 --- a/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt +++ b/legacy/src/androidTest/java/org/fdroid/fdroid/work/AppUpdateWorkerTest.kt @@ -18,6 +18,10 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.io.IOException +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail import kotlinx.coroutines.runBlocking import org.fdroid.fdroid.AppUpdateManager import org.fdroid.fdroid.FDroidApp @@ -31,12 +35,8 @@ import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_APP_UPD import org.fdroid.fdroid.work.AppUpdateWorker.Companion.UNIQUE_WORK_NAME_AUTO_APP_UPDATE import org.junit.Assume.assumeTrue import org.junit.Before +import org.junit.Test import org.junit.runner.RunWith -import java.io.IOException -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.test.fail @RunWith(AndroidJUnit4::class) class AppUpdateWorkerTest { diff --git a/libs/core/api/jvm/core.api b/libs/core/api/jvm/core.api new file mode 100644 index 000000000..4af8b61f5 --- /dev/null +++ b/libs/core/api/jvm/core.api @@ -0,0 +1,8 @@ +public abstract interface class org/fdroid/IndexFile { + public abstract fun getIpfsCidV1 ()Ljava/lang/String; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getSha256 ()Ljava/lang/String; + public abstract fun getSize ()Ljava/lang/Long; + public abstract fun serialize ()Ljava/lang/String; +} + diff --git a/libs/core/build.gradle.kts b/libs/core/build.gradle.kts index 1c209215c..955d12a04 100644 --- a/libs/core/build.gradle.kts +++ b/libs/core/build.gradle.kts @@ -12,11 +12,11 @@ kotlin { abiValidation { enabled = true } compilerOptions { optIn.add("kotlin.RequiresOptIn") } + jvm() android { namespace = "org.fdroid.core" compileSdk = libs.versions.compileSdk.get().toInt() minSdk = 21 - withJava() withHostTestBuilder {}.configure {} withDeviceTestBuilder { sourceSetTreeName = "test" } compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } diff --git a/libs/database/build.gradle.kts b/libs/database/build.gradle.kts index 2f5cdc3cf..77c59a0eb 100644 --- a/libs/database/build.gradle.kts +++ b/libs/database/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.android.library) alias(libs.plugins.android.ksp) alias(libs.plugins.jetbrains.dokka) @@ -26,14 +25,14 @@ android { } sourceSets { getByName("androidTest") { - java.srcDirs("src/dbTest/java") + kotlin.directories += "src/dbTest/java" // Adds exported schema location as test app assets. - assets.srcDirs(files("$projectDir/schemas")) + assets.directories += "$projectDir/schemas" } getByName("test") { - java.srcDirs("src/dbTest/java") + kotlin.directories += "src/dbTest/java" // Adds exported schema location as test app assets. - assets.srcDirs(files("$projectDir/schemas")) + assets.directories += "$projectDir/schemas" } } compileOptions { @@ -41,7 +40,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } testOptions { - targetSdk = 34 // relevant for instrumentation tests (targetSdk 21 fails on Android 14) unitTests { isIncludeAndroidResources = true } } androidResources { @@ -60,8 +58,6 @@ android { kotlin { explicitApi() - @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) - abiValidation { enabled = true } compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 optIn.add("kotlin.RequiresOptIn") diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt index 977ab4d57..55c9f4a90 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt @@ -2,6 +2,9 @@ package org.fdroid.database import androidx.core.os.LocaleListCompat import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail import org.fdroid.database.TestUtils.getOrFail import org.fdroid.database.TestUtils.toMetadataV2 import org.fdroid.test.TestRepoUtils.getRandomRepo @@ -9,215 +12,212 @@ 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) + @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()) + 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()) - } + 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 repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId3, packageName, app3, locales) + @Test + fun testGetSameAppFromTwoRepos() { + // insert same app into three repos (repoId1 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = 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) + // 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()) + // 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()) + // if repo is not given, app from repo with highest weight is returned + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // clear all apps - appDao.clearAll() - assertEquals(0, appDao.countApps()) - assertEquals(0, appDao.countLocalizedFiles()) - assertEquals(0, appDao.countLocalizedFileLists()) - } + // clear all apps + appDao.clearAll() + assertEquals(0, appDao.countApps()) + assertEquals(0, appDao.countLocalizedFiles()) + assertEquals(0, appDao.countLocalizedFileLists()) + } - @Test - fun testAppRepoPref() { - // insert same app into three repos (repoId1 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId3, packageName, app3, locales) + @Test + fun testAppRepoPref() { + // insert same app into three repos (repoId1 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId3, packageName, app3, locales) - // app from repo with highest weight is returned, if no prefs are set - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // app from repo with highest weight is returned, if no prefs are set + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // prefer repo3 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) - assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // prefer repo1 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // preferring non-existent repo for this app makes query fall back to highest weight repo - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = 1337L)) - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - } + // preferring non-existent repo for this app makes query fall back to highest weight repo + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = 1337L)) + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + } - @Test - fun testGetSameAppFromTwoReposOneDisabled() { - // insert same app into two repos (repoId2 has highest weight) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) + @Test + fun testGetSameAppFromTwoReposOneDisabled() { + // insert same app into two repos (repoId2 has highest weight) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) - // app from repo with highest weight gets returned - assertEquals(app2, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // app from repo with highest weight gets returned + assertEquals(app2, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // repo with highest weight gets disabled - repoDao.setRepositoryEnabled(repoId2, false) + // repo with highest weight gets disabled + repoDao.setRepositoryEnabled(repoId2, false) - // now app from repo with lower weight is returned - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - } + // now app from repo with lower weight is returned + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + } - @Test - fun testGetRepositoryIdsForApp() { - // initially, the app is in no repos - assertEquals(emptyList(), appDao.getRepositoryIdsForApp(packageName)) + @Test + fun testGetRepositoryIdsForApp() { + // initially, the app is in no repos + assertEquals(emptyList(), appDao.getRepositoryIdsForApp(packageName)) - // insert same app into one repo - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - assertEquals(listOf(repoId1), appDao.getRepositoryIdsForApp(packageName)) + // insert same app into one repo + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + assertEquals(listOf(repoId1), appDao.getRepositoryIdsForApp(packageName)) - // insert the app into one more repo - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId2, packageName, app2, locales) - assertEquals(listOf(repoId1, repoId2), appDao.getRepositoryIdsForApp(packageName)) + // insert the app into one more repo + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName, app2, locales) + assertEquals(listOf(repoId1, repoId2), appDao.getRepositoryIdsForApp(packageName)) - // when repo1 is disabled, it doesn't get returned anymore - repoDao.setRepositoryEnabled(repoId1, false) - assertEquals(listOf(repoId2), appDao.getRepositoryIdsForApp(packageName)) - } + // when repo1 is disabled, it doesn't get returned anymore + repoDao.setRepositoryEnabled(repoId1, false) + assertEquals(listOf(repoId2), appDao.getRepositoryIdsForApp(packageName)) + } - @Test - fun testUpdateCompatibility() { - // insert two apps with one version each - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) + @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) + // 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) + // 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) - } + // 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) + @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 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 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) - } + // 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()) + @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")) + // 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")) + // 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")) + // 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")) - // app1 as a variant of app2 in another repo will NOT show one more app in B - // because repo2 has less priority, so repo1 is preferred which doesn't have app1 in B - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId2, packageName2, app1, locales) - assertEquals(3, appDao.getNumberOfAppsInCategory("A")) - assertEquals(2, appDao.getNumberOfAppsInCategory("B")) - assertEquals(0, appDao.getNumberOfAppsInCategory("C")) - } + // app1 as a variant of app2 in another repo will NOT show one more app in B + // because repo2 has less priority, so repo1 is preferred which doesn't have app1 in B + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName2, app1, 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)) + @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, 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)) - } + appDao.insert(repoId, packageName2, app2, locales) + assertEquals(2, appDao.getNumberOfAppsInRepository(repoId)) + appDao.insert(repoId, packageName3, app3, locales) + assertEquals(3, appDao.getNumberOfAppsInRepository(repoId)) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt index d25bae15f..6dfaf67a7 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt @@ -6,6 +6,14 @@ import androidx.core.content.pm.PackageInfoCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every import io.mockk.mockk +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.fdroid.LocaleChooser.getBestLocale @@ -19,833 +27,800 @@ 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.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail @Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) internal class AppListItemsTest : AppTest() { - private val pm: PackageManager = mockk() + private val pm: PackageManager = mockk() - private val appPairs = listOf( - Pair(packageName1, app1), - Pair(packageName2, app2), - Pair(packageName3, app3), - ) + 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) + @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) - // one of the apps is installed - val packageInfo2 = PackageInfo().apply { - packageName = packageName2 - versionName = getRandomString() - versionCode = Random.nextInt(1, Int.MAX_VALUE) - } - every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) + // one of the apps is installed + val packageInfo2 = + PackageInfo().apply { + packageName = packageName2 + versionName = getRandomString() + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) - // 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 first app by partial search, sort by name - appDao.getAppListItems(pm, "On", NAME).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]) - assertEquals( - PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[0].installedVersionCode - ) - assertEquals(packageInfo2.versionName, apps[0].installedVersionName) - } - - // 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) - } + // 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]) } - @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) - - // one of the apps is installed - val packageInfo2 = PackageInfo().apply { - packageName = packageName2 - versionName = getRandomString() - versionCode = Random.nextInt(1, Int.MAX_VALUE) - } - every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) - - // 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]) - assertEquals( - PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[0].installedVersionCode - ) - assertEquals(packageInfo2.versionName, apps[0].installedVersionName) - } - - // 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) - } + // get first app by partial search, sort by name + appDao.getAppListItems(pm, "On", NAME).getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) } - @Test - fun testSearchQueryPerRepo() { - 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 app3a = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung")) - val app3b = app3.copy(name = mapOf("en-US" to "Three"), summary = mapOf("en" to "summary")) - // insert three apps in a random order - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName1, app1, locales) - appDao.insert(repoId2, packageName2, app2, locales) - appDao.insert(repoId2, packageName3, app3a, locales) - appDao.insert(repoId3, packageName3, app3b, locales) - - // one of the apps is installed - val packageInfo2 = PackageInfo().apply { - packageName = packageName2 - versionName = getRandomString() - versionCode = Random.nextInt(1, Int.MAX_VALUE) - } - every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) - - // get all apps in repo2 sorted by last updated - appDao.getAppListItems(pm, repoId2, null, LAST_UPDATED).getOrFail().let { apps -> - assertEquals(2, apps.size) - assertEquals(app3a, apps[0]) - assertEquals(app2, apps[1]) - assertEquals( - PackageInfoCompat.getLongVersionCode(packageInfo2), - apps[1].installedVersionCode - ) - assertEquals(packageInfo2.versionName, apps[1].installedVersionName) - } - - // get all apps in repo2 searching for summary - appDao.getAppListItems(pm, repoId2, "Zfassung", NAME).getOrFail().let { apps -> - assertEquals(2, apps.size) - val sortedApps = apps.sortedBy { it.lastUpdated } - assertEquals(app2, sortedApps[0]) - assertEquals(app3a, sortedApps[1]) - assertEquals( - PackageInfoCompat.getLongVersionCode(packageInfo2), - sortedApps[0].installedVersionCode - ) - assertEquals(packageInfo2.versionName, sortedApps[0].installedVersionName) - } - - // get first app by searching for summary - appDao.getAppListItems(pm, repoId1, "One", LAST_UPDATED).getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } - - // get third app by searching for summary in repo3 only - appDao.getAppListItems(pm, repoId3, "summary", LAST_UPDATED).getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app3b, apps[0]) - } - - // empty search for unknown category - appDao.getAppListItems(pm, 1337L, "Zfassung", LAST_UPDATED).getOrFail().let { apps -> - assertEquals(0, apps.size) - } - - // empty search for unknown search term - appDao.getAppListItems(pm, repoId2, "foo bar", LAST_UPDATED).getOrFail().let { apps -> - assertEquals(0, apps.size) - } + // 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]) + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), apps[0].installedVersionCode) + assertEquals(packageInfo2.versionName, apps[0].installedVersionName) } - @Test - fun testMalformedSearchQuery() { - every { pm.getInstalledPackages(0) } returns emptyList() - - // without category - appDao.getAppListItems(pm, "\"", LAST_UPDATED).getOrFail().let { apps -> - assertTrue(apps.isEmpty()) - } - appDao.getAppListItems(pm, "*simple\"*", NAME).getOrFail().let { apps -> - assertTrue(apps.isEmpty()) - } - - // with category - appDao.getAppListItems(pm, "Category", "\"", LAST_UPDATED).getOrFail().let { apps -> - assertTrue(apps.isEmpty()) - } - appDao.getAppListItems(pm, "Category", "*simple\"*", NAME).getOrFail().let { apps -> - assertTrue(apps.isEmpty()) - } + // 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]) + } } - @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) + // empty search for unknown search term + appDao.getAppListItems(pm, "foo bar", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) + } + } - // nothing is installed - every { pm.getInstalledPackages(0) } returns emptyList() + @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) - // 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]) - } - } + // one of the apps is installed + val packageInfo2 = + PackageInfo().apply { + packageName = packageName2 + versionName = getRandomString() + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) + + // 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]) } - @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]) - } - } + // 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]) + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), apps[0].installedVersionCode) + assertEquals(packageInfo2.versionName, apps[0].installedVersionName) } - @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 - 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( - PackageInfoCompat.getLongVersionCode(packageInfo2), - installed.installedVersionCode - ) - assertNull(other.installedVersionName) - assertNull(other.installedVersionCode) - } + // 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]) + } } - @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) - } - } + // 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]) } - @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) - } + // empty search for unknown category + appDao.getAppListItems(pm, "C", "Zfassung", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) } - @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) + // empty search for unknown search term + appDao.getAppListItems(pm, "A", "foo bar", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) + } + } - // initially both apps get returned - getItems { apps -> - assertEquals(2, apps.size) - } + @Test + fun testSearchQueryPerRepo() { + 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 app3a = app3.copy(name = mapOf("de-DE" to "Drei"), summary = mapOf("de" to "Zfassung")) + val app3b = app3.copy(name = mapOf("en-US" to "Three"), summary = mapOf("en" to "summary")) + // insert three apps in a random order + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName1, app1, locales) + appDao.insert(repoId2, packageName2, app2, locales) + appDao.insert(repoId2, packageName3, app3a, locales) + appDao.insert(repoId3, packageName3, app3b, locales) - // disable first repo - repoDao.setRepositoryEnabled(repoId, false) + // one of the apps is installed + val packageInfo2 = + PackageInfo().apply { + packageName = packageName2 + versionName = getRandomString() + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + every { pm.getInstalledPackages(0) } returns listOf(packageInfo2) - // now only app from enabled repo gets returned - getItems { apps -> - assertEquals(1, apps.size) - assertEquals(repoId2, apps[0].repoId) - } + // get all apps in repo2 sorted by last updated + appDao.getAppListItems(pm, repoId2, null, LAST_UPDATED).getOrFail().let { apps -> + assertEquals(2, apps.size) + assertEquals(app3a, apps[0]) + assertEquals(app2, apps[1]) + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo2), apps[1].installedVersionCode) + assertEquals(packageInfo2.versionName, apps[1].installedVersionName) } - @Test - fun testFromRepoWithHighestWeight() { - // insert same app into three repos (repoId1 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = 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]) - } + // get all apps in repo2 searching for summary + appDao.getAppListItems(pm, repoId2, "Zfassung", NAME).getOrFail().let { apps -> + assertEquals(2, apps.size) + val sortedApps = apps.sortedBy { it.lastUpdated } + assertEquals(app2, sortedApps[0]) + assertEquals(app3a, sortedApps[1]) + assertEquals( + PackageInfoCompat.getLongVersionCode(packageInfo2), + sortedApps[0].installedVersionCode, + ) + assertEquals(packageInfo2.versionName, sortedApps[0].installedVersionName) } - @Test - fun testFromRepoFromAppPrefs() { - // insert same app into three repos (repoId1 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId3, packageName, app3, locales) - - // app from repo1 with highest weight gets returned - getItems { apps -> - assertEquals(1, apps.size) - assertEquals(packageName, apps[0].packageName) - assertEquals(app1, apps[0]) - } - - // prefer repo3 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) - getItems { apps -> - assertEquals(1, apps.size) - assertEquals(packageName, apps[0].packageName) - assertEquals(app3, apps[0]) - } - - // prefer repo2 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) - getItems { apps -> - assertEquals(1, apps.size) - assertEquals(packageName, apps[0].packageName) - assertEquals(app2, apps[0]) - } - - // prefer repo1 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) - getItems { apps -> - assertEquals(1, apps.size) - assertEquals(packageName, apps[0].packageName) - assertEquals(app1, apps[0]) - } + // get first app by searching for summary + appDao.getAppListItems(pm, repoId1, "One", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) } - @Test - fun testOnlyFromGivenCategories() { - // nothing is installed - every { pm.getInstalledPackages(0) } returns emptyList() - - // 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.getAppListItems(pm, "B", null, NAME).getOrFail(), - appDao.getAppListItems(pm, "B", null, LAST_UPDATED).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.getAppListItems(pm, "C", null, NAME).getOrFail(), - appDao.getAppListItems(pm, "C", null, LAST_UPDATED).getOrFail(), - ).forEach { apps -> - assertEquals(0, apps.size) - } - - // we'll add app1 as a variant of app2, but its repo has lower weight, so no effect - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId2, packageName2, app1, locales) - listOf( - appDao.getAppListItems(pm, "B", null, NAME).getOrFail(), - appDao.getAppListItems(pm, "B", null, LAST_UPDATED).getOrFail(), - ).forEach { apps -> - assertEquals(2, apps.size) - assertNotEquals(packageName2, apps[0].packageName) - assertNotEquals(packageName2, apps[1].packageName) - } + // get third app by searching for summary in repo3 only + appDao.getAppListItems(pm, repoId3, "summary", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app3b, apps[0]) } - @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 - 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( - PackageInfoCompat.getLongVersionCode(packageInfo1), - 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) - } + // empty search for unknown category + appDao.getAppListItems(pm, 1337L, "Zfassung", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) } - @Test - fun testGetInstalledAppListItemsFlow() = runBlocking { - // 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) + // empty search for unknown search term + appDao.getAppListItems(pm, repoId2, "foo bar", LAST_UPDATED).getOrFail().let { apps -> + assertEquals(0, apps.size) + } + } - // define packageInfo for each test - 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 } + @Test + fun testMalformedSearchQuery() { + every { pm.getInstalledPackages(0) } returns emptyList() - // all apps get returned, if we consider all of them installed - val pmInfoMap1 = mapOf( - packageName1 to packageInfo1, - packageName2 to packageInfo2, - packageName3 to packageInfo3, - ) - assertEquals(3, appDao.getInstalledAppListItems(pmInfoMap1).first().size) - - // one apps get returned, if we consider only that one installed - val pmInfoMap2 = mapOf(packageName1 to packageInfo1) - appDao.getInstalledAppListItems(pmInfoMap2).first().let { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - // version code and version name gets taken from supplied packageInfo - assertEquals( - PackageInfoCompat.getLongVersionCode(packageInfo1), - apps[0].installedVersionCode - ) - assertEquals(packageInfo1.versionName, apps[0].installedVersionName) - } - - // no app gets returned, if we consider none installed - appDao.getInstalledAppListItems(emptyMap()).first().let { apps -> - assertEquals(0, apps.size) - } + // without category + appDao.getAppListItems(pm, "\"", LAST_UPDATED).getOrFail().let { apps -> + assertTrue(apps.isEmpty()) + } + appDao.getAppListItems(pm, "*simple\"*", NAME).getOrFail().let { apps -> + assertTrue(apps.isEmpty()) } - @Test - fun testGetInstalledAppListItemsMaxVars() { - // insert an app - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) - - val packageInfoCreator = { name: String -> - PackageInfo().apply { - packageName = name - versionName = name - versionCode = Random.nextInt(1, Int.MAX_VALUE) - } - } - val packageInfo = packageInfoCreator(packageName) - - // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query - // one additional package info is added to the package lists with each test case - val listPackageInfo = listOf(packageInfo) - val packageInfoOk = MutableList(998) { packageInfoCreator("${it + 1}") } - val packageInfoNotOk1 = MutableList(999) { packageInfoCreator("${it + 1}") } - val packageInfoNotOk2 = MutableList(5000) { packageInfoCreator("${it + 1}") } - - // app gets returned no matter how many packages are installed - every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo - assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) - every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo - assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) - every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo - assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) - - // ensure they have version info set - every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo - assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) - every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo - assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) - every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo - assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + // with category + appDao.getAppListItems(pm, "Category", "\"", LAST_UPDATED).getOrFail().let { apps -> + assertTrue(apps.isEmpty()) } - - @Test - fun testGetInstalledAppListItemsFlowMaxVars(): Unit = runBlocking { - // insert an app - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) - - val packageInfoCreator = { name: String -> - PackageInfo().apply { - packageName = name - versionName = name - versionCode = Random.nextInt(1, Int.MAX_VALUE) - } - } - val packageInfo = packageInfoCreator(packageName) - // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query - // one additional package info is added to the package lists with each test case - val packageInfoOk = mutableMapOf().apply { - for (i in 2..999) set("$i", packageInfoCreator("$i")) - set(packageName, packageInfo) - } - val packageInfoNotOk1 = mutableMapOf().apply { - for (i in 2..1000) set("$i", packageInfoCreator("$i")) - set(packageName, packageInfo) - } - val packageInfoNotOk2 = mutableMapOf().apply { - for (i in 2..5000) set("$i", packageInfoCreator("$i")) - set(packageName, packageInfo) - } - // app gets returned no matter how many packages are installed - assertEquals(1, appDao.getInstalledAppListItems(packageInfoOk).first().size) - assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk1).first().size) - assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk2).first().size) - - // ensure they have version info set - assertNotNull( - appDao.getInstalledAppListItems(packageInfoOk).first()[0].installedVersionName - ) - assertNotNull( - appDao.getInstalledAppListItems(packageInfoNotOk1).first()[0].installedVersionName - ) - assertNotNull( - appDao.getInstalledAppListItems(packageInfoNotOk2).first()[0].installedVersionName - ) + appDao.getAppListItems(pm, "Category", "*simple\"*", NAME).getOrFail().let { apps -> + assertTrue(apps.isEmpty()) } + } - // region author tests - @Test - fun testAuthor_NoApp() { - // should never happen, but test this just in case - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() + @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) - assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - assertTrue(appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail().isEmpty()) - assertTrue( - appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) - .getOrFail().isEmpty() - ) + // 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 testAuthor_OneApp() { - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() - val packageName = getRandomString() - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales) + @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) - assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() - assertEquals(1, appsForAuthor.size) + // 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 + 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( - 1, appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) - .getOrFail().size + PackageInfoCompat.getLongVersionCode(packageInfo2), + installed.installedVersionCode, ) - assertEquals(packageName, appsForAuthor[0].packageName) + 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) } - @Test - fun testAuthor_TwoApps() { - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() - val repoId = repoDao.insertOrReplace(getRandomRepo()) - val packageName1 = getRandomString() - val packageName2 = getRandomString() - val metadata2 = getRandomMetadataV2(author) - val metadata1 = getRandomMetadataV2(author, metadata2.lastUpdated + 1) - appDao.insert(repoId, packageName1, metadata1, locales) - appDao.insert(repoId, packageName2, metadata2, locales) + // each app gets a version + versionDao.insert(repoId, packageName1, "1", getRandomPackageVersionV2(), true) + versionDao.insert(repoId, packageName2, "1", getRandomPackageVersionV2(), false) - assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED) - .getOrFail() - assertEquals(2, appsForAuthor.size) - assertEquals(packageName1, appsForAuthor[0].packageName) - assertEquals(packageName2, appsForAuthor[1].packageName) + // 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) } - @Test - fun testAuthor_MultipleApps() { - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() - val repoId = repoDao.insertOrReplace(getRandomRepo()) - for (i in 1..50) { - appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales) - } + // app gets a version + val version1 = getRandomPackageVersionV2(42) + versionDao.insert(repoId, packageName1, "1", version1, true) - assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - assertEquals(50, appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail().size) + // 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) } - @Test - fun testHasAuthorMoreThanOneApp_OneAppDifferentRepos() { - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val packageName = getRandomString() - appDao.insert(repoId1, packageName, getRandomMetadataV2(author), locales) - val app4 = getRandomMetadataV2(author) - appDao.insert(repoId2, packageName, app4, locales) - appPrefsDao.update( - AppPrefs( - packageName = packageName, - preferredRepoId = repoId2, - ) - ) - assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() - assertEquals(1, appsForAuthor.size) - assertEquals(packageName, appsForAuthor[0].packageName) - assertEquals(repoId2, appsForAuthor[0].repoId) + // 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 repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = 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 testFromRepoFromAppPrefs() { + // insert same app into three repos (repoId1 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId3, packageName, app3, locales) + + // app from repo1 with highest weight gets returned + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app1, apps[0]) } - @Test - fun testHasAuthorMoreThanOneApp_TwoAppsDifferentRepos() { - every { pm.getInstalledPackages(0) } returns emptyList() - val author = getRandomString() - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, getRandomString(), getRandomMetadataV2(author), locales) - appDao.insert(repoId2, getRandomString(), getRandomMetadataV2(author), locales) - - assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() - assertEquals(2, appsForAuthor.size) - } - // endregion - - /** - * 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) -> Unit) { - every { pm.getInstalledPackages(0) } returns emptyList() - appDao.getAppListItemsByName().getOrFail().let(block) - appDao.getAppListItems(pm, "A", null, NAME).getOrFail().let(block) - appDao.getAppListItemsByLastUpdated().getOrFail().let(block) - appDao.getAppListItems(pm, "A", null, LAST_UPDATED).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) - } + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app3, apps[0]) } - 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)) + // prefer repo2 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app2, apps[0]) } + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app1, apps[0]) + } + } + + @Test + fun testOnlyFromGivenCategories() { + // nothing is installed + every { pm.getInstalledPackages(0) } returns emptyList() + + // 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.getAppListItems(pm, "B", null, NAME).getOrFail(), + appDao.getAppListItems(pm, "B", null, LAST_UPDATED).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.getAppListItems(pm, "C", null, NAME).getOrFail(), + appDao.getAppListItems(pm, "C", null, LAST_UPDATED).getOrFail(), + ) + .forEach { apps -> assertEquals(0, apps.size) } + + // we'll add app1 as a variant of app2, but its repo has lower weight, so no effect + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName2, app1, locales) + listOf( + appDao.getAppListItems(pm, "B", null, NAME).getOrFail(), + appDao.getAppListItems(pm, "B", null, LAST_UPDATED).getOrFail(), + ) + .forEach { apps -> + assertEquals(2, apps.size) + assertNotEquals(packageName2, apps[0].packageName) + assertNotEquals(packageName2, apps[1].packageName) + } + } + + @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 + 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(PackageInfoCompat.getLongVersionCode(packageInfo1), 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) } + } + + @Test + fun testGetInstalledAppListItemsFlow() = runBlocking { + // 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 + 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 + val pmInfoMap1 = + mapOf( + packageName1 to packageInfo1, + packageName2 to packageInfo2, + packageName3 to packageInfo3, + ) + assertEquals(3, appDao.getInstalledAppListItems(pmInfoMap1).first().size) + + // one apps get returned, if we consider only that one installed + val pmInfoMap2 = mapOf(packageName1 to packageInfo1) + appDao.getInstalledAppListItems(pmInfoMap2).first().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + // version code and version name gets taken from supplied packageInfo + assertEquals(PackageInfoCompat.getLongVersionCode(packageInfo1), apps[0].installedVersionCode) + assertEquals(packageInfo1.versionName, apps[0].installedVersionName) + } + + // no app gets returned, if we consider none installed + appDao.getInstalledAppListItems(emptyMap()).first().let { apps -> assertEquals(0, apps.size) } + } + + @Test + fun testGetInstalledAppListItemsMaxVars() { + // insert an app + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) + + val packageInfoCreator = { name: String -> + PackageInfo().apply { + packageName = name + versionName = name + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + } + val packageInfo = packageInfoCreator(packageName) + + // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query + // one additional package info is added to the package lists with each test case + val listPackageInfo = listOf(packageInfo) + val packageInfoOk = MutableList(998) { packageInfoCreator("${it + 1}") } + val packageInfoNotOk1 = MutableList(999) { packageInfoCreator("${it + 1}") } + val packageInfoNotOk2 = MutableList(5000) { packageInfoCreator("${it + 1}") } + + // app gets returned no matter how many packages are installed + every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo + assertEquals(1, appDao.getInstalledAppListItems(pm).getOrFail().size) + + // ensure they have version info set + every { pm.getInstalledPackages(0) } returns packageInfoOk + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk1 + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + every { pm.getInstalledPackages(0) } returns packageInfoNotOk2 + listPackageInfo + assertNotNull(appDao.getInstalledAppListItems(pm).getOrFail()[0].installedVersionName) + } + + @Test + fun testGetInstalledAppListItemsFlowMaxVars(): Unit = runBlocking { + // insert an app + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) + + val packageInfoCreator = { name: String -> + PackageInfo().apply { + packageName = name + versionName = name + versionCode = Random.nextInt(1, Int.MAX_VALUE) + } + } + val packageInfo = packageInfoCreator(packageName) + // sqlite (before 3.32.0) has a maximum number of 999 variables that can be used in a query + // one additional package info is added to the package lists with each test case + val packageInfoOk = + mutableMapOf().apply { + for (i in 2..999) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + val packageInfoNotOk1 = + mutableMapOf().apply { + for (i in 2..1000) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + val packageInfoNotOk2 = + mutableMapOf().apply { + for (i in 2..5000) set("$i", packageInfoCreator("$i")) + set(packageName, packageInfo) + } + // app gets returned no matter how many packages are installed + assertEquals(1, appDao.getInstalledAppListItems(packageInfoOk).first().size) + assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk1).first().size) + assertEquals(1, appDao.getInstalledAppListItems(packageInfoNotOk2).first().size) + + // ensure they have version info set + assertNotNull(appDao.getInstalledAppListItems(packageInfoOk).first()[0].installedVersionName) + assertNotNull( + appDao.getInstalledAppListItems(packageInfoNotOk1).first()[0].installedVersionName + ) + assertNotNull( + appDao.getInstalledAppListItems(packageInfoNotOk2).first()[0].installedVersionName + ) + } + + // region author tests + @Test + fun testAuthor_NoApp() { + // should never happen, but test this just in case + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + + assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + assertTrue(appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail().isEmpty()) + assertTrue( + appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED).getOrFail().isEmpty() + ) + } + + @Test + fun testAuthor_OneApp() { + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + val packageName = getRandomString() + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales) + + assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() + assertEquals(1, appsForAuthor.size) + assertEquals( + 1, + appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED).getOrFail().size, + ) + assertEquals(packageName, appsForAuthor[0].packageName) + } + + @Test + fun testAuthor_TwoApps() { + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + val repoId = repoDao.insertOrReplace(getRandomRepo()) + val packageName1 = getRandomString() + val packageName2 = getRandomString() + val metadata2 = getRandomMetadataV2(author) + val metadata1 = getRandomMetadataV2(author, metadata2.lastUpdated + 1) + appDao.insert(repoId, packageName1, metadata1, locales) + appDao.insert(repoId, packageName2, metadata2, locales) + + assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, LAST_UPDATED).getOrFail() + assertEquals(2, appsForAuthor.size) + assertEquals(packageName1, appsForAuthor[0].packageName) + assertEquals(packageName2, appsForAuthor[1].packageName) + } + + @Test + fun testAuthor_MultipleApps() { + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + val repoId = repoDao.insertOrReplace(getRandomRepo()) + for (i in 1..50) { + appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales) + } + + assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + assertEquals(50, appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail().size) + } + + @Test + fun testHasAuthorMoreThanOneApp_OneAppDifferentRepos() { + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val packageName = getRandomString() + appDao.insert(repoId1, packageName, getRandomMetadataV2(author), locales) + val app4 = getRandomMetadataV2(author) + appDao.insert(repoId2, packageName, app4, locales) + appPrefsDao.update(AppPrefs(packageName = packageName, preferredRepoId = repoId2)) + assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() + assertEquals(1, appsForAuthor.size) + assertEquals(packageName, appsForAuthor[0].packageName) + assertEquals(repoId2, appsForAuthor[0].repoId) + } + + @Test + fun testHasAuthorMoreThanOneApp_TwoAppsDifferentRepos() { + every { pm.getInstalledPackages(0) } returns emptyList() + val author = getRandomString() + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, getRandomString(), getRandomMetadataV2(author), locales) + appDao.insert(repoId2, getRandomString(), getRandomMetadataV2(author), locales) + + assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + val appsForAuthor = appDao.getAppListItemsForAuthor(pm, author, null, NAME).getOrFail() + assertEquals(2, appsForAuthor.size) + } + + // endregion + + /** + * 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) -> Unit) { + every { pm.getInstalledPackages(0) } returns emptyList() + appDao.getAppListItemsByName().getOrFail().let(block) + appDao.getAppListItems(pm, "A", null, NAME).getOrFail().let(block) + appDao.getAppListItemsByLastUpdated().getOrFail().let(block) + appDao.getAppListItems(pm, "A", null, LAST_UPDATED).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)) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt index c029272e5..cc4bc01b7 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt @@ -1,6 +1,12 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue import kotlinx.coroutines.runBlocking import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.database.TestUtils.getOrAwaitValue @@ -12,478 +18,463 @@ import org.fdroid.test.TestUtils.getRandomString import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2 import org.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) internal class AppOverviewItemsTest : AppTest() { - @Test - fun testAntiFeatures() = runBlocking { - // insert one app with without version - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) + @Test + fun testAntiFeatures() = runBlocking { + // insert one app with without version + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) - // without version, anti-features are empty - getItems().forEach { 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) - getItems().forEach { 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) - getItems().forEach { 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) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(version3.antiFeatures, apps[0].antiFeatures) - } + // without version, anti-features are empty + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertNull(apps[0].antiFeatures) } - @Test - fun testIcons() = runBlocking { - // insert one app - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) - - // icon is returned correctly - getItems().forEach { 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) - - // app is still returned as before - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales)) - } - - // after preferring second repo, icon is returned from app in second repo - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales)) - } + // with one version, the app has those anti-features + val version = getRandomPackageVersionV2(versionCode = 42) + versionDao.insert(repoId, packageName, "1", version, true) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(version.antiFeatures, apps[0].antiFeatures) } - @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) + // 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) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(version.antiFeatures, apps[0].antiFeatures) } - @Test - fun testIncompatibleFlag() = runBlocking { - // insert two apps - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName1, app1, locales) - appDao.insert(repoId, packageName2, app2, locales) + // 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) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(version3.antiFeatures, apps[0].antiFeatures) + } + } - // both apps are not compatible - getItems().forEach { apps -> - assertEquals(2, apps.size) - apps.forEach { - assertFalse(it.isCompatible) - } - } - // both apps, in the same category, are not compatible - appDao.getAppOverviewItems("A").getOrFail().also { - assertEquals(2, it.size) - }.forEach { - assertFalse(it.isCompatible) - } - assertFalse(appDao.getAppOverviewItem(repoId, packageName1)!!.isCompatible) - assertFalse(appDao.getAppOverviewItem(repoId, packageName2)!!.isCompatible) + @Test + fun testIcons() = runBlocking { + // insert one app + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) - // 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().forEach { apps -> - assertEquals(2, apps.size) - assertFalse(apps[0].isCompatible) - assertTrue(apps[1].isCompatible) - } - appDao.getAppOverviewItems("A").getOrFail().also { - assertEquals(2, it.size) - assertFalse(it[0].isCompatible) - assertTrue(it[1].isCompatible) - } - assertTrue(appDao.getAppOverviewItem(repoId, packageName1)!!.isCompatible) - assertFalse(appDao.getAppOverviewItem(repoId, packageName2)!!.isCompatible) + // icon is returned correctly + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales)) } - @Test - fun testGetByRepoWeight() = runBlocking { - // 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) + // insert same app into another repo + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName, app2, locales) - // app is returned correctly - getItems().forEach { 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) - - // app is still returned as before, new repo doesn't override old one - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } - - // now second app from second repo is returned after preferring it explicitly - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app2, apps[0]) - } + // app is still returned as before + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales)) } - @Test - fun testGetByRepoPref() = runBlocking { - // insert same app into three repos (repoId1 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId3, packageName, app3, locales) + // after preferring second repo, icon is returned from app in second repo + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales)) + } + } - // app is returned correctly from repo1 - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } - appDao.getAppOverviewItems("A").getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } + @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) - // prefer repo3 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app3, apps[0]) - } - appDao.getAppOverviewItems("B").getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app3, apps[0]) - } + // limit is respected + for (i in 0..3) assertEquals(i, appDao.getAppOverviewItems(i).getOrFail().size) + assertEquals(3, appDao.getAppOverviewItems(42).getOrFail().size) + } - // prefer repo2 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app2, apps[0]) - } - appDao.getAppOverviewItems("A").getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app2, apps[0]) - } - appDao.getAppOverviewItems("B").getOrFail().let { apps -> - assertEquals(0, apps.size) // app2 is not in category B - } + @Test + fun testIncompatibleFlag() = runBlocking { + // insert two apps + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName1, app1, locales) + appDao.insert(repoId, packageName2, app2, locales) - // prefer repo1 for this app - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) - getItems().forEach { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } - appDao.getAppOverviewItems("A").getOrFail().let { apps -> - assertEquals(1, apps.size) - assertEquals(app1, apps[0]) - } + // both apps are not compatible + getItems().forEach { apps -> + assertEquals(2, apps.size) + apps.forEach { assertFalse(it.isCompatible) } + } + // both apps, in the same category, are not compatible + appDao + .getAppOverviewItems("A") + .getOrFail() + .also { assertEquals(2, it.size) } + .forEach { assertFalse(it.isCompatible) } + assertFalse(appDao.getAppOverviewItem(repoId, packageName1)!!.isCompatible) + assertFalse(appDao.getAppOverviewItem(repoId, packageName2)!!.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().forEach { apps -> + assertEquals(2, apps.size) + assertFalse(apps[0].isCompatible) + assertTrue(apps[1].isCompatible) + } + appDao.getAppOverviewItems("A").getOrFail().also { + assertEquals(2, it.size) + assertFalse(it[0].isCompatible) + assertTrue(it[1].isCompatible) + } + assertTrue(appDao.getAppOverviewItem(repoId, packageName1)!!.isCompatible) + assertFalse(appDao.getAppOverviewItem(repoId, packageName2)!!.isCompatible) + } + + @Test + fun testGetByRepoWeight() = runBlocking { + // 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 + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1, 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) + // add another app without version + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName, app2, locales) - // 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 - // after we prefer that repo for this app - val app3b = app3.copy(icon = icons2) - appDao.insert(repoId2, packageName3, app3b) - appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) - // 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) - } + // app is still returned as before, new repo doesn't override old one + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) } - @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) + // now second app from second repo is returned after preferring it explicitly + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app2, apps[0]) + } + } - // 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()) - } + @Test + fun testGetByRepoPref() = runBlocking { + // insert same app into three repos (repoId1 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId3, packageName, app3, locales) - // 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 and is preferred, so is not last anymore - val app3b = app3.copy(icon = icons2) - appDao.insert(repoId2, packageName3, app3b) - appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) - // 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) + // app is returned correctly from repo1 + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) } - @Test - fun testOnlyFromEnabledRepos() = runBlocking { - 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 - getItems().forEach { apps -> - assertEquals(3, apps.size) - } - assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) - - // only 1 app after disabling first repo - repoDao.setRepositoryEnabled(repoId, false) - getItems().forEach { apps -> - assertEquals(1, apps.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) - getItems().forEach { apps -> - assertEquals(0, apps.size) - } - assertEquals(0, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) - assertEquals(0, appDao.getAppOverviewItems("B").getOrAwaitValue()?.size) + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app3, apps[0]) + } + appDao.getAppOverviewItems("B").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app3, apps[0]) } - @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)) + // prefer repo2 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app2, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app2, apps[0]) + } + appDao.getAppOverviewItems("B").getOrFail().let { apps -> + assertEquals(0, apps.size) // app2 is not in category B } - @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) + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + getItems().forEach { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + } - // each app gets returned properly - assertEquals(app1, appDao.getAppOverviewItem(repoId1, packageName)) - assertEquals(app2, appDao.getAppOverviewItem(repoId2, packageName)) + @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) - // 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)) + // 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()) } - @Test - fun testByAuthor() = runBlocking { - val author = getRandomString() - val packageName = getRandomString() - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales) - - // author has only one app - assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - assertEquals(0, appDao.getAppsByAuthor("foo bar").size) - appDao.getAppsByAuthor(author).let { apps -> - assertEquals(1, apps.size) - assertEquals(packageName, apps[0].packageName) - } - - // now add 49 more apps - (1 until 50).forEach { _ -> - appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales) - } - assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) - assertEquals(50, appDao.getAppsByAuthor(author).size) + // 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) } - @Test - fun testByCategory() = runBlocking { - // 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) + // 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) - // only two apps are in category B - appDao.getAppsByCategory("B").let { apps -> - assertEquals(2, apps.size) - assertNotEquals(packageName2, apps[0].packageName) - assertNotEquals(packageName2, apps[1].packageName) - } + // app3b is the same as app3, but has an icon, so is not last anymore + // after we prefer that repo for this app + val app3b = app3.copy(icon = icons2) + appDao.insert(repoId2, packageName3, app3b) + appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) + // 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) + } + } - // no app is in category C - assertEquals(0, appDao.getAppsByCategory("C").size) + @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) - // we'll add app1 as a variant of app2, but its repo has lower weight, so no effect - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId2, packageName2, app1, locales) - appDao.getAppsByCategory("B").let { apps -> - assertEquals(2, apps.size) - assertNotEquals(packageName2, apps[0].packageName) - assertNotEquals(packageName2, apps[1].packageName) - } + // 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()) } - private suspend fun getItems(): List> { - return listOf( - appDao.getAppOverviewItems().getOrFail(), - // manually sort the second list, so both results are comparable - appDao.getAllApps().sortedByDescending { it.lastUpdated }, - ) + // 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) } - 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.name.getBestLocale(locales), actual.getName(locales)) - assertEquals(expected.summary.getBestLocale(locales), actual.getSummary(locales)) - assertEquals(expected.icon.getBestLocale(locales), actual.getIcon(locales)) + // 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 and is preferred, so is not last anymore + val app3b = app3.copy(icon = icons2) + appDao.insert(repoId2, packageName3, app3b) + appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) + // 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() = runBlocking { + 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 + getItems().forEach { apps -> assertEquals(3, apps.size) } + assertEquals(3, appDao.getAppOverviewItems("A").getOrAwaitValue()?.size) + + // only 1 app after disabling first repo + repoDao.setRepositoryEnabled(repoId, false) + getItems().forEach { apps -> assertEquals(1, apps.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) + getItems().forEach { apps -> assertEquals(0, apps.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)) + } + + @Test + fun testByAuthor() = runBlocking { + val author = getRandomString() + val packageName = getRandomString() + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, getRandomMetadataV2(author), locales) + + // author has only one app + assertFalse(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + assertEquals(0, appDao.getAppsByAuthor("foo bar").size) + appDao.getAppsByAuthor(author).let { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + } + + // now add 49 more apps + (1 until 50).forEach { _ -> + appDao.insert(repoId, getRandomString(), getRandomMetadataV2(author), locales) + } + assertTrue(appDao.hasAuthorMoreThanOneApp(author).getOrFail()) + assertEquals(50, appDao.getAppsByAuthor(author).size) + } + + @Test + fun testByCategory() = runBlocking { + // 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 + appDao.getAppsByCategory("B").let { apps -> + assertEquals(2, apps.size) + assertNotEquals(packageName2, apps[0].packageName) + assertNotEquals(packageName2, apps[1].packageName) + } + + // no app is in category C + assertEquals(0, appDao.getAppsByCategory("C").size) + + // we'll add app1 as a variant of app2, but its repo has lower weight, so no effect + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName2, app1, locales) + appDao.getAppsByCategory("B").let { apps -> + assertEquals(2, apps.size) + assertNotEquals(packageName2, apps[0].packageName) + assertNotEquals(packageName2, apps[1].packageName) + } + } + + private suspend fun getItems(): List> { + return listOf( + appDao.getAppOverviewItems().getOrFail(), + // manually sort the second list, so both results are comparable + appDao.getAllApps().sortedByDescending { it.lastUpdated }, + ) + } + + 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.name.getBestLocale(locales), actual.getName(locales)) + assertEquals(expected.summary.getBestLocale(locales), actual.getSummary(locales)) + assertEquals(expected.icon.getBestLocale(locales), actual.getIcon(locales)) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppPrefsDaoTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppPrefsDaoTest.kt index 576982f36..0ffc548f3 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppPrefsDaoTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppPrefsDaoTest.kt @@ -1,159 +1,158 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail 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.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.test.fail @RunWith(AndroidJUnit4::class) internal class AppPrefsDaoTest : AppTest() { - @Test - fun testDisablingPreferredRepo() { - // insert same app into three repos (repoId3 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId2, packageName, app3, locales) + @Test + fun testDisablingPreferredRepo() { + // insert same app into three repos (repoId3 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId2, packageName, app3, locales) - // app from preferred repo gets returned - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // app from preferred repo gets returned + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - // preferred repo gets disabled - repoDao.setRepositoryEnabled(repoId1, false) + // preferred repo gets disabled + repoDao.setRepositoryEnabled(repoId1, false) - // now app from repo with highest weight is returned - assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // now app from repo with highest weight is returned + assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + } + + @Test + fun testRemovingPreferredRepo() { + // insert same app into three repos (repoId3 has highest weight) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId2, packageName, app3, locales) + + // app from preferred repo gets returned + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + + // preferred repo gets removed + repoDao.deleteRepository(repoId1) + + // now app from repo with highest weight is returned + assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + } + + @Test + fun testGetPreferredRepos() { + // insert three apps, the third is in repo2 and repo3 + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName1, app1, locales) + appDao.insert(repoId2, packageName2, app2, locales) + appDao.insert(repoId2, packageName3, app3, locales) + appDao.insert(repoId3, packageName3, app3, locales) + + // app1 and app2 are only in one repo, so that one is preferred + appPrefsDao.getPreferredRepos(listOf(packageName1, packageName2)).also { preferredRepos -> + assertEquals(2, preferredRepos.size) + assertEquals(repoId1, preferredRepos[packageName1]) + assertEquals(repoId2, preferredRepos[packageName2]) } - @Test - fun testRemovingPreferredRepo() { - // insert same app into three repos (repoId3 has highest weight) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) - appDao.insert(repoId2, packageName, app3, locales) - - // app from preferred repo gets returned - appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) - assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) - - // preferred repo gets removed - repoDao.deleteRepository(repoId1) - - // now app from repo with highest weight is returned - assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + // preference only based on global repo priority/weight (3>2>1) + appPrefsDao.getPreferredRepos(listOf(packageName3, packageName2)).also { preferredRepos -> + assertEquals(2, preferredRepos.size) + assertEquals(repoId2, preferredRepos[packageName2]) + assertEquals(repoId3, preferredRepos[packageName3]) } - @Test - fun testGetPreferredRepos() { - // insert three apps, the third is in repo2 and repo3 - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName1, app1, locales) - appDao.insert(repoId2, packageName2, app2, locales) - appDao.insert(repoId2, packageName3, app3, locales) - appDao.insert(repoId3, packageName3, app3, locales) - - // app1 and app2 are only in one repo, so that one is preferred - appPrefsDao.getPreferredRepos(listOf(packageName1, packageName2)).also { preferredRepos -> - assertEquals(2, preferredRepos.size) - assertEquals(repoId1, preferredRepos[packageName1]) - assertEquals(repoId2, preferredRepos[packageName2]) - } - - // preference only based on global repo priority/weight (3>2>1) - appPrefsDao.getPreferredRepos(listOf(packageName3, packageName2)).also { preferredRepos -> - assertEquals(2, preferredRepos.size) - assertEquals(repoId2, preferredRepos[packageName2]) - assertEquals(repoId3, preferredRepos[packageName3]) - } - - // now app3 prefers repo2 explicitly - appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) - appPrefsDao.getPreferredRepos(listOf(packageName3)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId2, preferredRepos[packageName3]) - } - - // repo2 removes app3 (oh no!), so preferred repo should fall back to repo3 again - appDao.deleteAppMetadata(repoId2, packageName3) - appPrefsDao.getPreferredRepos(listOf(packageName3)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId3, preferredRepos[packageName3]) - } - - // app3 prefers non-existing repo, so preferred repo should fall back to repo3 as well - appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = 1337)) - appPrefsDao.getPreferredRepos(listOf(packageName, packageName3)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId3, preferredRepos[packageName3]) - } - - // app3 moves back to preferring repo3 and query for non-existent package name as well - appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId3)) - appPrefsDao.getPreferredRepos(listOf(packageName, packageName3)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId3, preferredRepos[packageName3]) - } + // now app3 prefers repo2 explicitly + appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2)) + appPrefsDao.getPreferredRepos(listOf(packageName3)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId2, preferredRepos[packageName3]) } - @Test - fun getGetPreferredReposHandlesMaxVariableNumber() { - // sqlite has a maximum number of 999 variables that can be used in a query - val packagesOk = MutableList(998) { "" } + listOf(packageName) - val packagesNotOk1 = MutableList(1000) { "" } + listOf(packageName) - val packagesNotOk2 = MutableList(5000) { "" } + listOf(packageName) - - // insert same app in three repos - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1, locales) - appDao.insert(repoId, packageName, app2, locales) - appDao.insert(repoId, packageName, app3, locales) - - // preferred repos are returned as expected for all lists, no matter their size - assertEquals(1, appPrefsDao.getPreferredRepos(packagesOk).size) - assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk1).size) - assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk2).size) + // repo2 removes app3 (oh no!), so preferred repo should fall back to repo3 again + appDao.deleteAppMetadata(repoId2, packageName3) + appPrefsDao.getPreferredRepos(listOf(packageName3)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId3, preferredRepos[packageName3]) } - @Test - fun getGetPreferredReposIgnoresDisabledRepos() { - // insert one app 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) - - // repo1 has a higher priority than repo2 - val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail() - val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail() - assertTrue(repoPrefs1.weight > repoPrefs2.weight) - - // repo1 is preferred due to higher weight - appPrefsDao.getPreferredRepos(listOf(packageName)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId1, preferredRepos[packageName]) - } - - // disable repo1 - repoDao.updateRepositoryPreferences(repoPrefs1.copy(enabled = false)) - - // now repo2 is preferred, because only enabled repo for that app - appPrefsDao.getPreferredRepos(listOf(packageName)).also { preferredRepos -> - assertEquals(1, preferredRepos.size) - assertEquals(repoId2, preferredRepos[packageName]) - } + // app3 prefers non-existing repo, so preferred repo should fall back to repo3 as well + appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = 1337)) + appPrefsDao.getPreferredRepos(listOf(packageName, packageName3)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId3, preferredRepos[packageName3]) } + // app3 moves back to preferring repo3 and query for non-existent package name as well + appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId3)) + appPrefsDao.getPreferredRepos(listOf(packageName, packageName3)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId3, preferredRepos[packageName3]) + } + } + + @Test + fun getGetPreferredReposHandlesMaxVariableNumber() { + // sqlite has a maximum number of 999 variables that can be used in a query + val packagesOk = MutableList(998) { "" } + listOf(packageName) + val packagesNotOk1 = MutableList(1000) { "" } + listOf(packageName) + val packagesNotOk2 = MutableList(5000) { "" } + listOf(packageName) + + // insert same app in three repos + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1, locales) + appDao.insert(repoId, packageName, app2, locales) + appDao.insert(repoId, packageName, app3, locales) + + // preferred repos are returned as expected for all lists, no matter their size + assertEquals(1, appPrefsDao.getPreferredRepos(packagesOk).size) + assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk1).size) + assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk2).size) + } + + @Test + fun getGetPreferredReposIgnoresDisabledRepos() { + // insert one app 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) + + // repo1 has a higher priority than repo2 + val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail() + val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail() + assertTrue(repoPrefs1.weight > repoPrefs2.weight) + + // repo1 is preferred due to higher weight + appPrefsDao.getPreferredRepos(listOf(packageName)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId1, preferredRepos[packageName]) + } + + // disable repo1 + repoDao.updateRepositoryPreferences(repoPrefs1.copy(enabled = false)) + + // now repo2 is preferred, because only enabled repo for that app + appPrefsDao.getPreferredRepos(listOf(packageName)).also { preferredRepos -> + assertEquals(1, preferredRepos.size) + assertEquals(repoId2, preferredRepos[packageName]) + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppTest.kt index f4b41d9b7..f8ccb0ab7 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppTest.kt @@ -7,37 +7,45 @@ import org.fdroid.test.TestUtils.sort internal abstract class AppTest : DbTest() { - 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") + 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( + // 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( + 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( + categories = listOf("A"), + ) + .sort() + protected val app3 = + getRandomMetadataV2() + .copy( name = name3, icon = null, summary = name3, lastUpdated = 30, - categories = listOf("A", "B") - ).sort() - + categories = listOf("A", "B"), + ) + .sort() } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt index 011e7da5d..6617bebb5 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/CountryCodeMigrationTest.kt @@ -18,82 +18,93 @@ private const val TEST_DB = "migration-test" @RunWith(AndroidJUnit4::class) internal class CountryCodeMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - FDroidDatabaseInt::class.java, - listOf(CountryCodeMigration()), - FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + FDroidDatabaseInt::class.java, + listOf(CountryCodeMigration()), + FrameworkSQLiteOpenHelperFactory(), ) - private val repo = InitialRepository( - name = "F-Droid", - address = "https://f-droid.org/repo", - description = "The official F-Droid Free Software repository. " + - "Everything in this repository is always built from the source code.", - certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", - version = 13L, - enabled = true, - weight = 1, + private val repo = + InitialRepository( + name = "F-Droid", + address = "https://f-droid.org/repo", + description = + "The official F-Droid Free Software repository. " + + "Everything in this repository is always built from the source code.", + certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", + version = 13L, + enabled = true, + weight = 1, ) - @Test - fun migrateCountryCode() { - helper.createDatabase(TEST_DB, 7).use { db -> - // Database has schema version 7. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - val repoId = db.insert( - CoreRepository.TABLE, - SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("name", Converters.localizedTextV2toString(mapOf("en-US" to repo.name))) - put( - "description", - Converters.localizedTextV2toString(mapOf("en-US" to repo.description)) - ) - put("address", repo.address) - put("timestamp", 42) - put("certificate", repo.certificate) - }) - db.insert( - RepositoryPreferences.TABLE, - SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("enabled", repo.enabled) - put("lastETag", "foo") - put("weight", repo.weight) - }) - db.insert( - Mirror.TABLE, - SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("url", "foo") - put("location", "bar") - }) - } - - // Re-open the database with version 8, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 8, true).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - Room.databaseBuilder( - ApplicationProvider.getApplicationContext(), - FDroidDatabaseInt::class.java, - TEST_DB + @Test + fun migrateCountryCode() { + helper.createDatabase(TEST_DB, 7).use { db -> + // Database has schema version 7. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + val repoId = + db.insert( + CoreRepository.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", Converters.localizedTextV2toString(mapOf("en-US" to repo.name))) + put( + "description", + Converters.localizedTextV2toString(mapOf("en-US" to repo.description)), + ) + put("address", repo.address) + put("timestamp", 42) + put("certificate", repo.certificate) + }, ) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build().use { db -> - // check repo got timestamp and etag reset - val repos = db.getRepositoryDao().getRepositories() - assertEquals(1, repos.size) - assertEquals(null, repos[0].lastETag) - assertEquals(-1, repos[0].timestamp) - // check mirror - assertEquals(1, repos[0].mirrors.size) - val mirror = repos[0].mirrors[0] - assertEquals(repos[0].repoId, mirror.repoId) - assertEquals("foo", mirror.url) - assertEquals("bar", mirror.countryCode) - } + db.insert( + RepositoryPreferences.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("enabled", repo.enabled) + put("lastETag", "foo") + put("weight", repo.weight) + }, + ) + db.insert( + Mirror.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("url", "foo") + put("location", "bar") + }, + ) } + + // Re-open the database with version 8, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 8, true).close() + + // now get the Room DB, so we can use our DAOs for verifying the migration + Room.databaseBuilder( + ApplicationProvider.getApplicationContext(), + FDroidDatabaseInt::class.java, + TEST_DB, + ) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> + // check repo got timestamp and etag reset + val repos = db.getRepositoryDao().getRepositories() + assertEquals(1, repos.size) + assertEquals(null, repos[0].lastETag) + assertEquals(-1, repos[0].timestamp) + // check mirror + assertEquals(1, repos[0].mirrors.size) + val mirror = repos[0].mirrors[0] + assertEquals(repos[0].repoId, mirror.repoId) + assertEquals("foo", mirror.url) + assertEquals("bar", mirror.countryCode) + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt index e4b31214c..b86939fc9 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt @@ -9,6 +9,10 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider.getApplicationContext import io.mockk.every import io.mockk.mockkObject +import java.io.IOException +import java.util.Locale +import kotlin.test.assertEquals +import kotlin.test.fail import kotlinx.coroutines.Dispatchers import org.fdroid.database.TestUtils.assertRepoEquals import org.fdroid.database.TestUtils.getOrFail @@ -17,6 +21,7 @@ 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.getRes import org.fdroid.test.TestUtils.sort import org.fdroid.test.TestUtils.sorted import org.fdroid.test.VerifierConstants.CERTIFICATE @@ -24,119 +29,115 @@ import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Rule -import java.io.IOException -import java.util.Locale -import kotlin.test.assertEquals -import kotlin.test.fail internal abstract class DbTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() - 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 + 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 - private val context: Context = getApplicationContext() - protected val assets: AssetManager = context.resources.assets - protected val locales = LocaleListCompat.create(Locale.US) + private 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() + @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() - // pre-Android P limitations for instrumentation tests (unit tests w/ robolectric are fine): - // https://mockk.io/ANDROID#supported-features - // See also: https://github.com/mockk/mockk/issues/182 - assumeTrue(Build.MODEL == "robolectric" || Build.VERSION.SDK_INT >= 28) + // pre-Android P limitations for instrumentation tests (unit tests w/ robolectric are fine): + // https://mockk.io/ANDROID#supported-features + // See also: https://github.com/mockk/mockk/issues/182 + assumeTrue(Build.MODEL == "robolectric" || Build.VERSION.SDK_INT >= 28) - mockkObject(FDroidDatabaseHolder) - every { FDroidDatabaseHolder.dispatcher } returns testCoroutineDispatcher + 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, certificate = certificate) + val streamReceiver = DbV1StreamReceiver(db, repoId) { true } + val indexProcessor = IndexV1StreamProcessor(streamReceiver, lastTimestamp) + db.runInTransaction { + getRes(indexAssetPath).use { indexStream -> indexProcessor.process(indexStream) } } + return repoId + } - @After - @Throws(IOException::class) - fun closeDb() { - db.close() + 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, certificate = certificate) + val streamReceiver = DbV2StreamReceiver(db, repoId) { true } + val indexProcessor = IndexV2FullStreamProcessor(streamReceiver) + db.runInTransaction { + getRes(indexAssetPath).use { indexStream -> + indexProcessor.process(version, indexStream) {} + } } + return repoId + } - 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, certificate = certificate) - val streamReceiver = DbV1StreamReceiver(db, repoId) { true } - val indexProcessor = IndexV1StreamProcessor(streamReceiver, lastTimestamp) - db.runInTransaction { - assets.open(indexAssetPath).use { indexStream -> - indexProcessor.process(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( + // zero-whitespace hack needs to get applied to expected data + packageV2.metadata.copy( + name = packageV2.metadata.name.zero(), + summary = packageV2.metadata.summary.zero(), + description = packageV2.metadata.description.zero(), + ), + appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort(), + ) + val versions = + versionDao + .getAppVersions(repoId, packageName) + .getOrFail() + .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() + // filter out duplicate entries from index data + val v = + packageVersionV2.copy( + manifest = + packageVersionV2.manifest.copy( + usesPermission = packageVersionV2.manifest.usesPermission.toSet().toList() + ) + ) + assertEquals(v, version) + } } - - 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, certificate = certificate) - val streamReceiver = DbV2StreamReceiver(db, repoId) { true } - val indexProcessor = IndexV2FullStreamProcessor(streamReceiver) - 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( - // zero-whitespace hack needs to get applied to expected data - packageV2.metadata.copy( - name = packageV2.metadata.name.zero(), - summary = packageV2.metadata.summary.zero(), - description = packageV2.metadata.description.zero(), - ), - appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort() - ) - val versions = versionDao.getAppVersions(repoId, packageName).getOrFail().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() - // filter out duplicate entries from index data - val v = packageVersionV2.copy( - manifest = packageVersionV2.manifest.copy( - usesPermission = packageVersionV2.manifest.usesPermission.toSet().toList() - ) - ) - assertEquals(v, version) - } - } - } - + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt index 57327014c..a1e9cdf16 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/DbUpdateCheckerTest.kt @@ -6,6 +6,12 @@ import android.content.pm.PackageManager.NameNotFoundException import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every import io.mockk.mockk +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail import org.fdroid.index.RELEASE_CHANNEL_BETA import org.fdroid.index.v2.PackageVersionV2 import org.fdroid.index.v2.SignerV2 @@ -16,321 +22,308 @@ import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2 import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail @Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) internal class DbUpdateCheckerTest : AppTest() { - private lateinit var updateChecker: DbUpdateChecker - private val packageManager: PackageManager = mockk() - private val compatChecker: (PackageVersionV2) -> Boolean = { true } + private lateinit var updateChecker: DbUpdateChecker + private val packageManager: PackageManager = mockk() + private val compatChecker: (PackageVersionV2) -> Boolean = { true } - private val packageInfo = PackageInfo().apply { - packageName = TestDataMinV2.PACKAGE_NAME - versionCode = 0 + private val packageInfo = + PackageInfo().apply { + packageName = TestDataMinV2.PACKAGE_NAME + versionCode = 0 } - @Before - override fun createDb() { - super.createDb() - every { packageManager.systemAvailableFeatures } returns emptyArray() - updateChecker = DbUpdateChecker(db, packageManager, { true }) + @Before + override fun createDb() { + super.createDb() + every { packageManager.systemAvailableFeatures } returns emptyArray() + updateChecker = DbUpdateChecker(db, packageManager, { true }) + } + + @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 testSuggestedVersionRespectsPreferredSigner() { + // insert one app into the repo + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, app1.copy(), locales) + + // two version have two different signers (correct format doesn't matter here) + val signer1 = SignerV2(listOf("foo", "bar")) + val signer2 = SignerV2(listOf("justOneSigner")) + + // add two versions with the same version code, but different signers + val packageVersion1 = getRandomPackageVersionV2(versionCode = 42) + val packageVersion2 = getRandomPackageVersionV2(versionCode = 42) + val versionId1 = packageVersion1.file.sha256 + val versionId2 = packageVersion2.file.sha256 + val version1 = + packageVersion1 + .copy( + manifest = packageVersion1.manifest.copy(signer = signer1), + releaseChannels = emptyList(), + ) + .toVersion(repoId, packageName, versionId1, true) + val version2 = + packageVersion2 + .copy( + manifest = packageVersion2.manifest.copy(signer = signer2), + releaseChannels = emptyList(), + ) + .toVersion(repoId, packageName, versionId2, true) + versionDao.insert(version1) + versionDao.insert(version2) + + // nothing is currently installed + every { packageManager.getPackageInfo(packageName, any()) } returns null + + // if signer of first version is preferred first version is suggested as update + assertEquals( + version1, + updateChecker + .getSuggestedVersion(packageName = packageName, preferredSigner = signer1.sha256[0]) + ?.version, + ) + + // if second signer of first version is preferred first version is suggested as update + assertEquals( + version1, + updateChecker + .getSuggestedVersion(packageName = packageName, preferredSigner = signer1.sha256[1]) + ?.version, + ) + + // if signer of second version is preferred second version is suggested as update + assertEquals( + version2, + updateChecker + .getSuggestedVersion(packageName = packageName, preferredSigner = signer2.sha256[0]) + ?.version, + ) + } + + @Test + fun testSuggestedVersionOnlyFromPreferredRepo() { + // insert the same app into two repos + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + + // every app has a compatible version + val packageVersion1 = + mapOf("1" to getRandomPackageVersionV2(2, null).copy(releaseChannels = emptyList())) + val packageVersion2 = + mapOf("2" to getRandomPackageVersionV2(1, null).copy(releaseChannels = emptyList())) + versionDao.insert(repoId1, packageName, packageVersion1, compatChecker) + versionDao.insert(repoId2, packageName, packageVersion2, compatChecker) + + // nothing is installed + every { packageManager.getPackageInfo(packageName, any()) } throws NameNotFoundException() + + // without preferring repos, version with highest version code gets returned + updateChecker.getSuggestedVersion(packageName).also { appVersion -> + assertNotNull(appVersion) + assertEquals(repoId1, appVersion.repoId) + assertEquals(2, appVersion.manifest.versionCode) } - @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) + // now we want versions only from preferred repo and get the one with highest weight + updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true).also { appVersion + -> + assertNotNull(appVersion) + assertEquals(repoId2, appVersion.repoId) + assertEquals(1, appVersion.manifest.versionCode) } - @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) + // now we allow all repos, but explicitly prefer repo 1, getting same result as above + appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1)) + updateChecker.getSuggestedVersion(packageName).also { appVersion -> + assertNotNull(appVersion) + assertEquals(repoId1, appVersion.repoId) + assertEquals(2, appVersion.manifest.versionCode) } - @Test - fun testSuggestedVersionRespectsPreferredSigner() { - // insert one app into the repo - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, app1.copy(), locales) - - // two version have two different signers (correct format doesn't matter here) - val signer1 = SignerV2(listOf("foo", "bar")) - val signer2 = SignerV2(listOf("justOneSigner")) - - // add two versions with the same version code, but different signers - val packageVersion1 = getRandomPackageVersionV2(versionCode = 42) - val packageVersion2 = getRandomPackageVersionV2(versionCode = 42) - val versionId1 = packageVersion1.file.sha256 - val versionId2 = packageVersion2.file.sha256 - val version1 = packageVersion1.copy( - manifest = packageVersion1.manifest.copy(signer = signer1), - releaseChannels = emptyList(), - ).toVersion(repoId, packageName, versionId1, true) - val version2 = packageVersion2.copy( - manifest = packageVersion2.manifest.copy(signer = signer2), - releaseChannels = emptyList(), - ).toVersion(repoId, packageName, versionId2, true) - versionDao.insert(version1) - versionDao.insert(version2) - - // nothing is currently installed - every { packageManager.getPackageInfo(packageName, any()) } returns null - - // if signer of first version is preferred first version is suggested as update - assertEquals( - version1, - updateChecker.getSuggestedVersion( - packageName = packageName, - preferredSigner = signer1.sha256[0] - )?.version, - ) - - // if second signer of first version is preferred first version is suggested as update - assertEquals( - version1, - updateChecker.getSuggestedVersion( - packageName = packageName, - preferredSigner = signer1.sha256[1] - )?.version, - ) - - // if signer of second version is preferred second version is suggested as update - assertEquals( - version2, - updateChecker.getSuggestedVersion( - packageName = packageName, - preferredSigner = signer2.sha256[0] - )?.version, - ) + // now we prefer repo 2 and only want versions from preferred repo + appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2)) + updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true).also { appVersion + -> + assertNotNull(appVersion) + assertEquals(repoId2, appVersion.repoId) + assertEquals(1, appVersion.manifest.versionCode) } - @Test - fun testSuggestedVersionOnlyFromPreferredRepo() { - // insert the same app into two repos - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageName, app1, locales) - appDao.insert(repoId2, packageName, app2, locales) + // now we have version 1 already installed + every { packageManager.getPackageInfo(packageName, any()) } returns + PackageInfo().apply { + packageName = this@DbUpdateCheckerTest.packageName + versionCode = 1 + } - // every app has a compatible version - val packageVersion1 = mapOf( - "1" to getRandomPackageVersionV2(2, null).copy(releaseChannels = emptyList()) - ) - val packageVersion2 = mapOf( - "2" to getRandomPackageVersionV2(1, null).copy(releaseChannels = emptyList()) - ) - versionDao.insert(repoId1, packageName, packageVersion1, compatChecker) - versionDao.insert(repoId2, packageName, packageVersion2, compatChecker) - - // nothing is installed - every { - packageManager.getPackageInfo(packageName, any()) - } throws NameNotFoundException() - - // without preferring repos, version with highest version code gets returned - updateChecker.getSuggestedVersion(packageName).also { appVersion -> - assertNotNull(appVersion) - assertEquals(repoId1, appVersion.repoId) - assertEquals(2, appVersion.manifest.versionCode) - } - - // now we want versions only from preferred repo and get the one with highest weight - updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true) - .also { appVersion -> - assertNotNull(appVersion) - assertEquals(repoId2, appVersion.repoId) - assertEquals(1, appVersion.manifest.versionCode) - } - - // now we allow all repos, but explicitly prefer repo 1, getting same result as above - appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1)) - updateChecker.getSuggestedVersion(packageName).also { appVersion -> - assertNotNull(appVersion) - assertEquals(repoId1, appVersion.repoId) - assertEquals(2, appVersion.manifest.versionCode) - } - - // now we prefer repo 2 and only want versions from preferred repo - appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2)) - updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true) - .also { appVersion -> - assertNotNull(appVersion) - assertEquals(repoId2, appVersion.repoId) - assertEquals(1, appVersion.manifest.versionCode) - } - - // now we have version 1 already installed - every { - packageManager.getPackageInfo(packageName, any()) - } returns PackageInfo().apply { - packageName = this@DbUpdateCheckerTest.packageName - versionCode = 1 - } - - // preferred repos don't have suggested versions - updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true) - .also { appVersion -> - assertNull(appVersion) - } - - // but other repos still have - updateChecker.getSuggestedVersion(packageName).also { appVersion -> - assertNotNull(appVersion) - assertEquals(repoId1, appVersion.repoId) - assertEquals(2, appVersion.manifest.versionCode) - } + // preferred repos don't have suggested versions + updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true).also { appVersion + -> + assertNull(appVersion) } - @Test - fun testGetUpdatableApps() { - streamIndexV2IntoDb("index-min-v2.json") - every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo) + // but other repos still have + updateChecker.getSuggestedVersion(packageName).also { appVersion -> + assertNotNull(appVersion) + assertEquals(repoId1, appVersion.repoId) + assertEquals(2, appVersion.manifest.versionCode) + } + } - val appVersions = updateChecker.getUpdatableApps() - assertEquals(1, appVersions.size) - assertEquals(0, appVersions[0].installedVersionCode) - assertEquals(TestDataMinV2.PACKAGE_NAME, appVersions[0].packageName) - assertEquals(TestDataMinV2.version.file.sha256, appVersions[0].update.version.versionId) + @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.PACKAGE_NAME, appVersions[0].packageName) + assertEquals(TestDataMinV2.version.file.sha256, appVersions[0].update.version.versionId) + } + + @Test + fun testGetUpdatableAppsOnlyFromPreferredRepo() { + // insert the same app into three repos + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageInfo.packageName, app1, locales) + appDao.insert(repoId2, packageInfo.packageName, app2, locales) + appDao.insert(repoId3, packageInfo.packageName, app3, locales) + + // every app has a compatible update (versionCode greater than 0) + val packageVersion1 = + mapOf("1" to getRandomPackageVersionV2(13, null).copy(releaseChannels = emptyList())) + val packageVersion2 = + mapOf("2" to getRandomPackageVersionV2(12, null).copy(releaseChannels = emptyList())) + val packageVersion3 = + mapOf("3" to getRandomPackageVersionV2(10, null).copy(releaseChannels = emptyList())) + versionDao.insert(repoId1, packageInfo.packageName, packageVersion1, compatChecker) + versionDao.insert(repoId2, packageInfo.packageName, packageVersion2, compatChecker) + versionDao.insert(repoId3, packageInfo.packageName, packageVersion3, compatChecker) + + // app is installed with version code 0 + assertEquals(0, packageInfo.versionCode) + every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo) + + // without preferring repos, version with highest version code gets returned + updateChecker.getUpdatableApps().also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId1, appVersions[0].repoId) + assertEquals(13, appVersions[0].update.manifest.versionCode) + assertFalse(appVersions[0].isFromPreferredRepo) // preferred repo is 3 per weight } - @Test - fun testGetUpdatableAppsOnlyFromPreferredRepo() { - // insert the same app into three repos - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageInfo.packageName, app1, locales) - appDao.insert(repoId2, packageInfo.packageName, app2, locales) - appDao.insert(repoId3, packageInfo.packageName, app3, locales) - - // every app has a compatible update (versionCode greater than 0) - val packageVersion1 = mapOf( - "1" to getRandomPackageVersionV2(13, null).copy(releaseChannels = emptyList()) - ) - val packageVersion2 = mapOf( - "2" to getRandomPackageVersionV2(12, null).copy(releaseChannels = emptyList()) - ) - val packageVersion3 = mapOf( - "3" to getRandomPackageVersionV2(10, null).copy(releaseChannels = emptyList()) - ) - versionDao.insert(repoId1, packageInfo.packageName, packageVersion1, compatChecker) - versionDao.insert(repoId2, packageInfo.packageName, packageVersion2, compatChecker) - versionDao.insert(repoId3, packageInfo.packageName, packageVersion3, compatChecker) - - // app is installed with version code 0 - assertEquals(0, packageInfo.versionCode) - every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo) - - // without preferring repos, version with highest version code gets returned - updateChecker.getUpdatableApps().also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId1, appVersions[0].repoId) - assertEquals(13, appVersions[0].update.manifest.versionCode) - assertFalse(appVersions[0].isFromPreferredRepo) // preferred repo is 3 per weight - } - - // now we want versions only from preferred repo and get the one with highest weight - updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId3, appVersions[0].repoId) - assertEquals(10, appVersions[0].update.manifest.versionCode) - assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 3 due to weight - } - - // now we allow all repos, but explicitly prefer repo 1, isFromPreferredRepo becomes true - appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1)) - updateChecker.getUpdatableApps().also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId1, appVersions[0].repoId) - assertEquals(13, appVersions[0].update.manifest.versionCode) - assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 1 now - } - - // now we prefer repo 2 and only want versions from preferred repo - appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2)) - updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId2, appVersions[0].repoId) - assertEquals(12, appVersions[0].update.manifest.versionCode) - assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 2 now - } + // now we want versions only from preferred repo and get the one with highest weight + updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId3, appVersions[0].repoId) + assertEquals(10, appVersions[0].update.manifest.versionCode) + assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 3 due to weight } - @Test - fun testGetUpdatableAppsUnaffectedByDisabledRepos() { - // insert the same app into two repos - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId1, packageInfo.packageName, app1, locales) - appDao.insert(repoId2, packageInfo.packageName, app2, locales) - - // repo1 has a higher priority than repo2 - val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail() - val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail() - assertTrue(repoPrefs1.weight > repoPrefs2.weight) - - // both apps a compatible update (versionCode greater than 0) - val packageVersion1 = mapOf( - "1" to getRandomPackageVersionV2(12, null).copy(releaseChannels = emptyList()) - ) - val packageVersion2 = mapOf( - "2" to getRandomPackageVersionV2(13, null).copy(releaseChannels = emptyList()) - ) - versionDao.insert(repoId1, packageInfo.packageName, packageVersion1, compatChecker) - versionDao.insert(repoId2, packageInfo.packageName, packageVersion2, compatChecker) - - // app is installed with version code 0 - assertEquals(0, packageInfo.versionCode) - every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo) - - // version from repo with highest priority (1) gets returned - updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId1, appVersions[0].repoId) - assertEquals(12, appVersions[0].update.manifest.versionCode) - assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 1 per weight - } - - // disable repo1 - repoDao.updateRepositoryPreferences(repoPrefs1.copy(enabled = false)) - - // now update from remaining enabled repo should get returned - updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> - assertEquals(1, appVersions.size) - assertEquals(repoId2, appVersions[0].repoId) - assertEquals(13, appVersions[0].update.manifest.versionCode) - assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 2 - } + // now we allow all repos, but explicitly prefer repo 1, isFromPreferredRepo becomes true + appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1)) + updateChecker.getUpdatableApps().also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId1, appVersions[0].repoId) + assertEquals(13, appVersions[0].update.manifest.versionCode) + assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 1 now } + // now we prefer repo 2 and only want versions from preferred repo + appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2)) + updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId2, appVersions[0].repoId) + assertEquals(12, appVersions[0].update.manifest.versionCode) + assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 2 now + } + } + + @Test + fun testGetUpdatableAppsUnaffectedByDisabledRepos() { + // insert the same app into two repos + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageInfo.packageName, app1, locales) + appDao.insert(repoId2, packageInfo.packageName, app2, locales) + + // repo1 has a higher priority than repo2 + val repoPrefs1 = repoDao.getRepositoryPreferences(repoId1) ?: fail() + val repoPrefs2 = repoDao.getRepositoryPreferences(repoId2) ?: fail() + assertTrue(repoPrefs1.weight > repoPrefs2.weight) + + // both apps a compatible update (versionCode greater than 0) + val packageVersion1 = + mapOf("1" to getRandomPackageVersionV2(12, null).copy(releaseChannels = emptyList())) + val packageVersion2 = + mapOf("2" to getRandomPackageVersionV2(13, null).copy(releaseChannels = emptyList())) + versionDao.insert(repoId1, packageInfo.packageName, packageVersion1, compatChecker) + versionDao.insert(repoId2, packageInfo.packageName, packageVersion2, compatChecker) + + // app is installed with version code 0 + assertEquals(0, packageInfo.versionCode) + every { packageManager.getInstalledPackages(any()) } returns listOf(packageInfo) + + // version from repo with highest priority (1) gets returned + updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId1, appVersions[0].repoId) + assertEquals(12, appVersions[0].update.manifest.versionCode) + assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 1 per weight + } + + // disable repo1 + repoDao.updateRepositoryPreferences(repoPrefs1.copy(enabled = false)) + + // now update from remaining enabled repo should get returned + updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions -> + assertEquals(1, appVersions.size) + assertEquals(repoId2, appVersions[0].repoId) + assertEquals(13, appVersions[0].update.manifest.versionCode) + assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 2 + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/FDroidDatabaseTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/FDroidDatabaseTest.kt index 072977fb5..e23ddba0f 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/FDroidDatabaseTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/FDroidDatabaseTest.kt @@ -1,44 +1,39 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import org.fdroid.test.TestRepoUtils import org.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals @RunWith(AndroidJUnit4::class) internal class FDroidDatabaseTest : AppTest() { - @Test - fun testClearAllAppData() { - // insert three apps in two repos - val repoId2 = repoDao.insertOrReplace(TestRepoUtils.getRandomRepo()) - val repoId1 = repoDao.insertOrReplace(TestRepoUtils.getRandomRepo()) - appDao.insert(repoId1, packageName1, app1, locales) - appDao.insert(repoId2, packageName2, app2, locales) - appDao.insert(repoId2, packageName3, app3, locales) + @Test + fun testClearAllAppData() { + // insert three apps in two repos + val repoId2 = repoDao.insertOrReplace(TestRepoUtils.getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(TestRepoUtils.getRandomRepo()) + appDao.insert(repoId1, packageName1, app1, locales) + appDao.insert(repoId2, packageName2, app2, locales) + appDao.insert(repoId2, packageName3, app3, locales) - // assert that both repos and all three apps made it into the DB - assertEquals(2, repoDao.getRepositories().size) - assertEquals(3, appDao.countApps()) + // assert that both repos and all three apps made it into the DB + assertEquals(2, repoDao.getRepositories().size) + assertEquals(3, appDao.countApps()) - // assert that repo timestamps are recent, not reset - repoDao.getRepositories().forEach { repo -> - assertNotEquals(-1, repo.timestamp) - } + // assert that repo timestamps are recent, not reset + repoDao.getRepositories().forEach { repo -> assertNotEquals(-1, repo.timestamp) } - // clear all app data - db.clearAllAppData() + // clear all app data + db.clearAllAppData() - // assert that both repos survived, but all apps are gone - assertEquals(2, repoDao.getRepositories().size) - assertEquals(0, appDao.countApps()) - - // assert that repo timestamps got reset - repoDao.getRepositories().forEach { repo -> - assertEquals(-1, repo.timestamp) - } - } + // assert that both repos survived, but all apps are gone + assertEquals(2, repoDao.getRepositories().size) + assertEquals(0, appDao.countApps()) + // assert that repo timestamps got reset + repoDao.getRepositories().forEach { repo -> assertEquals(-1, repo.timestamp) } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt index 3e006705f..b9e0670c2 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/FtsAddColumnsMigrationTest.kt @@ -11,230 +11,229 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import kotlin.random.Random +import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.fdroid.database.TestUtils.getOrFail import org.fdroid.test.TestUtils.getRandomString import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import kotlin.random.Random -import kotlin.test.assertEquals private const val TEST_DB = "fts-test" @RunWith(AndroidJUnit4::class) internal class FtsAddColumnsMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - instrumentation = InstrumentationRegistry.getInstrumentation(), - databaseClass = FDroidDatabaseInt::class.java, - specs = emptyList(), - openFactory = FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + instrumentation = InstrumentationRegistry.getInstrumentation(), + databaseClass = FDroidDatabaseInt::class.java, + specs = emptyList(), + openFactory = FrameworkSQLiteOpenHelperFactory(), ) - @get:Rule - val instantExec = InstantTaskExecutorRule() + @get:Rule val instantExec = InstantTaskExecutorRule() - private val context: Context = ApplicationProvider.getApplicationContext() + private val context: Context = ApplicationProvider.getApplicationContext() - private val repo = ContentValues().apply { - put("repoId", 1) - put("name", Converters.localizedTextV2toString(mapOf("de" to "a", "en-US" to "b"))) - put("address", getRandomString()) - put("certificate", "abcdef") - put("description", Converters.localizedTextV2toString(mapOf("de" to "aa", "en-US" to "bb"))) - put("version", Random.nextLong()) - put("timestamp", Random.nextLong()) + private val repo = + ContentValues().apply { + put("repoId", 1) + put("name", Converters.localizedTextV2toString(mapOf("de" to "a", "en-US" to "b"))) + put("address", getRandomString()) + put("certificate", "abcdef") + put("description", Converters.localizedTextV2toString(mapOf("de" to "aa", "en-US" to "bb"))) + put("version", Random.nextLong()) + put("timestamp", Random.nextLong()) } - private val repoPrefs = ContentValues().apply { - put("repoId", 1) - put("enabled", true) - put("weight", 1) + private val repoPrefs = + ContentValues().apply { + put("repoId", 1) + put("enabled", true) + put("weight", 1) } - private val oeffiMetadata = ContentValues().apply { - put("packageName", "de.schildbach.oeffi") - put("repoId", 1) - put( - "name", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffi", "en-US" to "Offi" - ) - ) - ) - put( - "description", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport" - ) - ) - ) - put("license", "GPL-3.0") - put( - "summary", Converters.localizedTextV2toString( - mapOf( - "de" to "Der König des Fahrplandschungels!", - "en-US" to " King of public transit planning!" - ) - ) - ) - put("localizedName", "Öffi") - put("localizedSummary", "Der König des Fahrplandschungels!") - put("added", Random.nextLong()) - put("lastUpdated", Random.nextLong()) - put("isCompatible", true) + private val oeffiMetadata = + ContentValues().apply { + put("packageName", "de.schildbach.oeffi") + put("repoId", 1) + put("name", Converters.localizedTextV2toString(mapOf("de" to "Öffi", "en-US" to "Offi"))) + put( + "description", + Converters.localizedTextV2toString( + mapOf("de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport") + ), + ) + put("license", "GPL-3.0") + put( + "summary", + Converters.localizedTextV2toString( + mapOf( + "de" to "Der König des Fahrplandschungels!", + "en-US" to " King of public transit planning!", + ) + ), + ) + put("localizedName", "Öffi") + put("localizedSummary", "Der König des Fahrplandschungels!") + put("added", Random.nextLong()) + put("lastUpdated", Random.nextLong()) + put("isCompatible", true) } - private val oeffiVersion = ContentValues().apply { - put("repoId", 1) - put("packageName", "de.schildbach.oeffi") - put("versionId", 1) - put("added", 1000) - put("isCompatible", true) - put("file_name", "transportr.apk") - put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") - put("manifest_versionName", "1.0.0") - put("manifest_versionCode", 1) + private val oeffiVersion = + ContentValues().apply { + put("repoId", 1) + put("packageName", "de.schildbach.oeffi") + put("versionId", 1) + put("added", 1000) + put("isCompatible", true) + put("file_name", "transportr.apk") + put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") + put("manifest_versionName", "1.0.0") + put("manifest_versionCode", 1) } - private val transportrMetadata = ContentValues().apply { - put("packageName", "de.grobox.liberario") - put("repoId", 1) - put( - "name", Converters.localizedTextV2toString( - mapOf( - "de" to "Transportr", "en-US" to "Transportr" - ) - ) - ) - put( - "description", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport" - ) - ) - ) - put("license", "GPL-3.0") - put( - "summary", Converters.localizedTextV2toString( - mapOf( - "de" to "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung", - "en-US" to "Free Public Transport Assistant without Ads or Tracking" - ) - ) - ) - put("localizedName", "Transportr") - put( - "localizedSummary", - "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung und Tracking" - ) - put("added", Random.nextLong()) - put("lastUpdated", Random.nextLong()) - put("isCompatible", true) + private val transportrMetadata = + ContentValues().apply { + put("packageName", "de.grobox.liberario") + put("repoId", 1) + put( + "name", + Converters.localizedTextV2toString(mapOf("de" to "Transportr", "en-US" to "Transportr")), + ) + put( + "description", + Converters.localizedTextV2toString( + mapOf("de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport") + ), + ) + put("license", "GPL-3.0") + put( + "summary", + Converters.localizedTextV2toString( + mapOf( + "de" to "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung", + "en-US" to "Free Public Transport Assistant without Ads or Tracking", + ) + ), + ) + put("localizedName", "Transportr") + put( + "localizedSummary", + "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung und Tracking", + ) + put("added", Random.nextLong()) + put("lastUpdated", Random.nextLong()) + put("isCompatible", true) } - private val transportrVersion = ContentValues().apply { - put("repoId", 1) - put("packageName", "de.grobox.liberario") - put("versionId", 1) - put("added", 1000) - put("isCompatible", true) - put("file_name", "transportr.apk") - put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") - put("manifest_versionName", "1.0.0") - put("manifest_versionCode", 1) + private val transportrVersion = + ContentValues().apply { + put("repoId", 1) + put("packageName", "de.grobox.liberario") + put("versionId", 1) + put("added", 1000) + put("isCompatible", true) + put("file_name", "transportr.apk") + put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") + put("manifest_versionName", "1.0.0") + put("manifest_versionCode", 1) } - @Test - fun testMigration() = runBlocking { - helper.createDatabase(TEST_DB, 8).use { db -> - // Database has schema version 8. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - db.insert(CoreRepository.TABLE, CONFLICT_FAIL, repo) - db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, repoPrefs) - db.insert(AppMetadata.TABLE, CONFLICT_FAIL, oeffiMetadata) - db.insert(AppMetadata.TABLE, CONFLICT_FAIL, transportrMetadata) - db.insert(Version.TABLE, CONFLICT_FAIL, oeffiVersion) - db.insert(Version.TABLE, CONFLICT_FAIL, transportrVersion) + @Test + fun testMigration() = runBlocking { + helper.createDatabase(TEST_DB, 8).use { db -> + // Database has schema version 8. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + db.insert(CoreRepository.TABLE, CONFLICT_FAIL, repo) + db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, repoPrefs) + db.insert(AppMetadata.TABLE, CONFLICT_FAIL, oeffiMetadata) + db.insert(AppMetadata.TABLE, CONFLICT_FAIL, transportrMetadata) + db.insert(Version.TABLE, CONFLICT_FAIL, oeffiVersion) + db.insert(Version.TABLE, CONFLICT_FAIL, transportrVersion) - // default search with no diacritics - assertSearch(db, "*Transport*", 1) - assertSearch(db, "Transportr", 1) - assertSearch(db, "*f*", 2) - // no or wrong diacritics - assertSearch(db, "Offi", 0) - assertSearch(db, "offi", 0) - assertSearch(db, "õffi", 0) - assertSearch(db, "*Offi*", 0) - assertSearch(db, "*offi*", 0) - assertSearch(db, "Konig", 0) - // correct diacritics - assertSearch(db, "*Öffi*", 1) - assertSearch(db, "*öffi*", 1) - assertSearch(db, "Öffi", 1) - assertSearch(db, "öffi", 1) - // both apps have "öff" in their name or summary - assertSearch(db, "*öff*", 2) - assertSearch(db, "König", 1) - } - - helper.runMigrationsAndValidate(TEST_DB, 9, true, MIGRATION_8_9).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB) - .allowMainThreadQueries() - .build().use { db -> - // assert that apps are still there - val metadata = db.getAppDao().getAppMetadata() - assertEquals(2, metadata.size) - // default search with no diacritics - assertGetAppListItems(db, "*Transport*", 2) - assertGetAppListItems(db, "Transportr", 1) - assertGetAppListItems(db, "*f*", 2) - // no or wrong diacritics also produces results now - assertGetAppListItems(db, "Offi", 1) - assertGetAppListItems(db, "offi", 1) - assertGetAppListItems(db, "õffi", 1) - assertGetAppListItems(db, "*Offi*", 1) - assertGetAppListItems(db, "*offi*", 1) - assertGetAppListItems(db, "Konig", 1) - // correct diacritics still produces results - assertGetAppListItems(db, "*Öffi*", 1) - assertGetAppListItems(db, "*öffi*", 1) - assertGetAppListItems(db, "Öffi", 1) - assertGetAppListItems(db, "öffi", 1) - // both apps have "öff" in their name or summary - assertGetAppListItems(db, "*öff*", 2) - assertGetAppListItems(db, "König", 1) - - // a new app also gets updated in the index - assertGetAppListItems(db, "Rosa", 0) - assertGetAppListItems(db, "Elefant", 0) - val newApp = AppMetadata( - repoId = 1, - packageName = "org.example", - added = 23, - lastUpdated = 42, - name = mapOf("de_DE" to "Rosa Elefant"), - isCompatible = true - ) - db.getAppDao().insert(newApp) - assertGetAppListItems(db, "Rosa", 1) - assertGetAppListItems(db, "Elefant", 1) - } + // default search with no diacritics + assertSearch(db, "*Transport*", 1) + assertSearch(db, "Transportr", 1) + assertSearch(db, "*f*", 2) + // no or wrong diacritics + assertSearch(db, "Offi", 0) + assertSearch(db, "offi", 0) + assertSearch(db, "õffi", 0) + assertSearch(db, "*Offi*", 0) + assertSearch(db, "*offi*", 0) + assertSearch(db, "Konig", 0) + // correct diacritics + assertSearch(db, "*Öffi*", 1) + assertSearch(db, "*öffi*", 1) + assertSearch(db, "Öffi", 1) + assertSearch(db, "öffi", 1) + // both apps have "öff" in their name or summary + assertSearch(db, "*öff*", 2) + assertSearch(db, "König", 1) } - private fun assertSearch(db: SupportSQLiteDatabase, query: String, expected: Int) { - db.query("SELECT * FROM AppMetadataFts WHERE AppMetadataFts MATCH '$query'") - .use { cursor -> assertEquals(expected, cursor.count) } - } + helper.runMigrationsAndValidate(TEST_DB, 9, true, MIGRATION_8_9).close() - private fun assertGetAppListItems(db: FDroidDatabaseInt, query: String, expected: Int) { - db.getAppDao().getAppListItems(query).getOrFail().let { result -> - assertEquals(expected, result.size, "${result.map { it.name }}") - } - } + // now get the Room DB, so we can use our DAOs for verifying the migration + Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB) + .allowMainThreadQueries() + .build() + .use { db -> + // assert that apps are still there + val metadata = db.getAppDao().getAppMetadata() + assertEquals(2, metadata.size) + // default search with no diacritics + assertGetAppListItems(db, "*Transport*", 2) + assertGetAppListItems(db, "Transportr", 1) + assertGetAppListItems(db, "*f*", 2) + // no or wrong diacritics also produces results now + assertGetAppListItems(db, "Offi", 1) + assertGetAppListItems(db, "offi", 1) + assertGetAppListItems(db, "õffi", 1) + assertGetAppListItems(db, "*Offi*", 1) + assertGetAppListItems(db, "*offi*", 1) + assertGetAppListItems(db, "Konig", 1) + // correct diacritics still produces results + assertGetAppListItems(db, "*Öffi*", 1) + assertGetAppListItems(db, "*öffi*", 1) + assertGetAppListItems(db, "Öffi", 1) + assertGetAppListItems(db, "öffi", 1) + // both apps have "öff" in their name or summary + assertGetAppListItems(db, "*öff*", 2) + assertGetAppListItems(db, "König", 1) + // a new app also gets updated in the index + assertGetAppListItems(db, "Rosa", 0) + assertGetAppListItems(db, "Elefant", 0) + val newApp = + AppMetadata( + repoId = 1, + packageName = "org.example", + added = 23, + lastUpdated = 42, + name = mapOf("de_DE" to "Rosa Elefant"), + isCompatible = true, + ) + db.getAppDao().insert(newApp) + assertGetAppListItems(db, "Rosa", 1) + assertGetAppListItems(db, "Elefant", 1) + } + } + + private fun assertSearch(db: SupportSQLiteDatabase, query: String, expected: Int) { + db.query("SELECT * FROM AppMetadataFts WHERE AppMetadataFts MATCH '$query'").use { cursor -> + assertEquals(expected, cursor.count) + } + } + + private fun assertGetAppListItems(db: FDroidDatabaseInt, query: String, expected: Int) { + db.getAppDao().getAppListItems(query).getOrFail().let { result -> + assertEquals(expected, result.size, "${result.map { it.name }}") + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt index 815db1845..c5bd9fcde 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/FtsCaseInsensitiveMigrationTest.kt @@ -11,193 +11,188 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import kotlin.random.Random +import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.fdroid.database.TestUtils.getOrFail import org.fdroid.test.TestUtils.getRandomString import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import kotlin.random.Random -import kotlin.test.assertEquals private const val TEST_DB = "fts-test" @RunWith(AndroidJUnit4::class) internal class FtsCaseInsensitiveMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - instrumentation = InstrumentationRegistry.getInstrumentation(), - databaseClass = FDroidDatabaseInt::class.java, - specs = emptyList(), - openFactory = FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + instrumentation = InstrumentationRegistry.getInstrumentation(), + databaseClass = FDroidDatabaseInt::class.java, + specs = emptyList(), + openFactory = FrameworkSQLiteOpenHelperFactory(), ) - @get:Rule - val instantExec = InstantTaskExecutorRule() + @get:Rule val instantExec = InstantTaskExecutorRule() - private val context: Context = ApplicationProvider.getApplicationContext() + private val context: Context = ApplicationProvider.getApplicationContext() - private val repo = ContentValues().apply { - put("repoId", 1) - put("name", Converters.localizedTextV2toString(mapOf("de" to "a", "en-US" to "b"))) - put("address", getRandomString()) - put("certificate", "abcdef") - put("description", Converters.localizedTextV2toString(mapOf("de" to "aa", "en-US" to "bb"))) - put("version", Random.nextLong()) - put("timestamp", Random.nextLong()) + private val repo = + ContentValues().apply { + put("repoId", 1) + put("name", Converters.localizedTextV2toString(mapOf("de" to "a", "en-US" to "b"))) + put("address", getRandomString()) + put("certificate", "abcdef") + put("description", Converters.localizedTextV2toString(mapOf("de" to "aa", "en-US" to "bb"))) + put("version", Random.nextLong()) + put("timestamp", Random.nextLong()) } - private val repoPrefs = ContentValues().apply { - put("repoId", 1) - put("enabled", true) - put("weight", 1) + private val repoPrefs = + ContentValues().apply { + put("repoId", 1) + put("enabled", true) + put("weight", 1) } - private val oeffiMetadata = ContentValues().apply { - put("packageName", "de.schildbach.oeffi") - put("repoId", 1) - put( - "name", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffi", "en-US" to "Offi" - ) - ) - ) - put( - "description", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport" - ) - ) - ) - put("license", "GPL-3.0") - put( - "summary", Converters.localizedTextV2toString( - mapOf( - "de" to "Der König des Fahrplandschungels!", - "en-US" to " King of public transit planning!" - ) - ) - ) - put("localizedName", "Öffi") - put("localizedSummary", "Der König des Fahrplandschungels!") - put("added", Random.nextLong()) - put("lastUpdated", Random.nextLong()) - put("isCompatible", true) + private val oeffiMetadata = + ContentValues().apply { + put("packageName", "de.schildbach.oeffi") + put("repoId", 1) + put("name", Converters.localizedTextV2toString(mapOf("de" to "Öffi", "en-US" to "Offi"))) + put( + "description", + Converters.localizedTextV2toString( + mapOf("de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport") + ), + ) + put("license", "GPL-3.0") + put( + "summary", + Converters.localizedTextV2toString( + mapOf( + "de" to "Der König des Fahrplandschungels!", + "en-US" to " King of public transit planning!", + ) + ), + ) + put("localizedName", "Öffi") + put("localizedSummary", "Der König des Fahrplandschungels!") + put("added", Random.nextLong()) + put("lastUpdated", Random.nextLong()) + put("isCompatible", true) } - private val oeffiVersion = ContentValues().apply { - put("repoId", 1) - put("packageName", "de.schildbach.oeffi") - put("versionId", 1) - put("added", 1000) - put("isCompatible", true) - put("file_name", "transportr.apk") - put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") - put("manifest_versionName", "1.0.0") - put("manifest_versionCode", 1) + private val oeffiVersion = + ContentValues().apply { + put("repoId", 1) + put("packageName", "de.schildbach.oeffi") + put("versionId", 1) + put("added", 1000) + put("isCompatible", true) + put("file_name", "transportr.apk") + put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") + put("manifest_versionName", "1.0.0") + put("manifest_versionCode", 1) } - private val transportrMetadata = ContentValues().apply { - put("packageName", "de.grobox.liberario") - put("repoId", 1) - put( - "name", Converters.localizedTextV2toString( - mapOf( - "de" to "Transportr", "en-US" to "Transportr" - ) - ) - ) - put( - "description", Converters.localizedTextV2toString( - mapOf( - "de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport" - ) - ) - ) - put("license", "GPL-3.0") - put( - "summary", Converters.localizedTextV2toString( - mapOf( - "de" to "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung", - "en-US" to "Free Public Transport Assistant without Ads or Tracking" - ) - ) - ) - put("localizedName", "Transportr") - put( - "localizedSummary", - "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung und Tracking" - ) - put("added", Random.nextLong()) - put("lastUpdated", Random.nextLong()) - put("isCompatible", true) + private val transportrMetadata = + ContentValues().apply { + put("packageName", "de.grobox.liberario") + put("repoId", 1) + put( + "name", + Converters.localizedTextV2toString(mapOf("de" to "Transportr", "en-US" to "Transportr")), + ) + put( + "description", + Converters.localizedTextV2toString( + mapOf("de" to "Öffentlicher Nahverkehr", "en-US" to "Public Transport") + ), + ) + put("license", "GPL-3.0") + put( + "summary", + Converters.localizedTextV2toString( + mapOf( + "de" to "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung", + "en-US" to "Free Public Transport Assistant without Ads or Tracking", + ) + ), + ) + put("localizedName", "Transportr") + put( + "localizedSummary", + "Freier Assistent für den öffentlichen Nahverkehr ohne Werbung und Tracking", + ) + put("added", Random.nextLong()) + put("lastUpdated", Random.nextLong()) + put("isCompatible", true) } - private val transportrVersion = ContentValues().apply { - put("repoId", 1) - put("packageName", "de.grobox.liberario") - put("versionId", 1) - put("added", 1000) - put("isCompatible", true) - put("file_name", "transportr.apk") - put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") - put("manifest_versionName", "1.0.0") - put("manifest_versionCode", 1) + private val transportrVersion = + ContentValues().apply { + put("repoId", 1) + put("packageName", "de.grobox.liberario") + put("versionId", 1) + put("added", 1000) + put("isCompatible", true) + put("file_name", "transportr.apk") + put("file_sha256", "73475CB40A568E8DA8A045CED110137E159F890AC4DA883B6B17DC651B3A8049") + put("manifest_versionName", "1.0.0") + put("manifest_versionCode", 1) } - @Test - fun testMigration() = runBlocking { - helper.createDatabase(TEST_DB, 5).use { db -> - // Database has schema version 5. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - db.insert(CoreRepository.TABLE, SQLiteDatabase.CONFLICT_FAIL, repo) - db.insert(RepositoryPreferences.TABLE, SQLiteDatabase.CONFLICT_FAIL, repoPrefs) - db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiMetadata) - db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, transportrMetadata) - db.insert(Version.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiVersion) - db.insert(Version.TABLE, SQLiteDatabase.CONFLICT_FAIL, transportrVersion) + @Test + fun testMigration() = runBlocking { + helper.createDatabase(TEST_DB, 5).use { db -> + // Database has schema version 5. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + db.insert(CoreRepository.TABLE, SQLiteDatabase.CONFLICT_FAIL, repo) + db.insert(RepositoryPreferences.TABLE, SQLiteDatabase.CONFLICT_FAIL, repoPrefs) + db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiMetadata) + db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, transportrMetadata) + db.insert(Version.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiVersion) + db.insert(Version.TABLE, SQLiteDatabase.CONFLICT_FAIL, transportrVersion) - // Show that search is case sensitive for diacritics - assertSearch(db, "Öffi", 1) - assertSearch(db, "öffi", 0) - // using no diacritics does match any case - assertSearch(db, "Offi", 0) - assertSearch(db, "offi", 0) - // it's case sensitive so only "öffentlichen" from Transportr is found - assertSearch(db, "öff*", 1) - assertSearch(db, "König", 1) - - } - - helper.runMigrationsAndValidate(TEST_DB, 6, true, MIGRATION_5_6).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB) - .allowMainThreadQueries() - .addMigrations(MIGRATION_8_9) // was added later - .build().use { db -> - // assert that apps are still there - val metadata = db.getAppDao().getAppMetadata() - assertEquals(2, metadata.size) - // default search with no diacritics - assertGetAppListItems(db, "*Transport*", 2) - - // other tests here were removed, because MIGRATION_8_9 changed this once again - // and has its own test - } + // Show that search is case sensitive for diacritics + assertSearch(db, "Öffi", 1) + assertSearch(db, "öffi", 0) + // using no diacritics does match any case + assertSearch(db, "Offi", 0) + assertSearch(db, "offi", 0) + // it's case sensitive so only "öffentlichen" from Transportr is found + assertSearch(db, "öff*", 1) + assertSearch(db, "König", 1) } - private fun assertSearch(db: SupportSQLiteDatabase, query: String, expected: Int) { - db.query("SELECT * FROM AppMetadataFts WHERE AppMetadataFts MATCH '$query'") - .use { cursor -> assertEquals(expected, cursor.count) } - } + helper.runMigrationsAndValidate(TEST_DB, 6, true, MIGRATION_5_6).close() - private fun assertGetAppListItems(db: FDroidDatabaseInt, query: String, expected: Int) { - db.getAppDao().getAppListItems(query).getOrFail().let { - assertEquals(expected, it.size) - } - } + // now get the Room DB, so we can use our DAOs for verifying the migration + Room.databaseBuilder(context, FDroidDatabaseInt::class.java, TEST_DB) + .allowMainThreadQueries() + .addMigrations(MIGRATION_8_9) // was added later + .build() + .use { db -> + // assert that apps are still there + val metadata = db.getAppDao().getAppMetadata() + assertEquals(2, metadata.size) + // default search with no diacritics + assertGetAppListItems(db, "*Transport*", 2) + // other tests here were removed, because MIGRATION_8_9 changed this once again + // and has its own test + } + } + + private fun assertSearch(db: SupportSQLiteDatabase, query: String, expected: Int) { + db.query("SELECT * FROM AppMetadataFts WHERE AppMetadataFts MATCH '$query'").use { cursor -> + assertEquals(expected, cursor.count) + } + } + + private fun assertGetAppListItems(db: FDroidDatabaseInt, query: String, expected: Int) { + db.getAppDao().getAppListItems(query).getOrFail().let { assertEquals(expected, it.size) } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/IndexV1InsertTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/IndexV1InsertTest.kt index 0f2c515c3..093f83f84 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/IndexV1InsertTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/IndexV1InsertTest.kt @@ -1,6 +1,9 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue import kotlinx.serialization.SerializationException import org.apache.commons.io.input.CountingInputStream import org.fdroid.index.IndexConverter @@ -16,119 +19,109 @@ import org.fdroid.test.TestDataEmptyV1 import org.fdroid.test.TestDataMaxV1 import org.fdroid.test.TestDataMidV1 import org.fdroid.test.TestDataMinV1 +import org.fdroid.test.TestUtils.getRes 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() + 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 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, -1) + db.runInTransaction { + getRes(path).use { indexStream -> indexProcessor.process(indexStream) } } + return repoId + } - @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 { + @Test + fun testExceptionWhileStreamingDoesNotSaveIntoDb() { + val cIn = CountingInputStream(getRes("index-max-v1.json")) + assertFailsWith { + db.runInTransaction { val repoId = db.getRepositoryDao().insertEmptyRepo("https://f-droid.org/repo") - val streamReceiver = TestStreamReceiver(repoId) + val streamReceiver = + TestStreamReceiver(repoId) { if (cIn.byteCount > 0) throw SerializationException() } val indexProcessor = IndexV1StreamProcessor(streamReceiver, -1) - db.runInTransaction { - assets.open(path).use { indexStream -> - indexProcessor.process(indexStream) - } - } - return repoId + 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) { + streamReceiver.receive(repo, version) + callback() } - @Test - fun testExceptionWhileStreamingDoesNotSaveIntoDb() { - val cIn = CountingInputStream(assets.open("index-max-v1.json")) - assertFailsWith { - 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, -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) + override fun receive(packageName: String, m: MetadataV2) { + streamReceiver.receive(packageName, m) + callback() } - @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) { - streamReceiver.receive(repo, version) - callback() - } - - override fun receive(packageName: String, m: MetadataV2) { - streamReceiver.receive(packageName, m) - callback() - } - - override fun receive(packageName: String, v: Map) { - streamReceiver.receive(packageName, v) - callback() - } - - override fun updateRepo( - antiFeatures: Map, - categories: Map, - releaseChannels: Map, - ) { - streamReceiver.updateRepo(antiFeatures, categories, releaseChannels) - callback() - } - - override fun updateAppMetadata(packageName: String, preferredSigner: String?) { - streamReceiver.updateAppMetadata(packageName, preferredSigner) - callback() - } - + override fun receive(packageName: String, v: Map) { + streamReceiver.receive(packageName, v) + callback() } + override fun updateRepo( + antiFeatures: Map, + categories: Map, + releaseChannels: Map, + ) { + streamReceiver.updateRepo(antiFeatures, categories, releaseChannels) + callback() + } + + override fun updateAppMetadata(packageName: String, preferredSigner: String?) { + streamReceiver.updateAppMetadata(packageName, preferredSigner) + callback() + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt index f0e4db3c7..695cd3c54 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2DiffTest.kt @@ -1,6 +1,9 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.io.ByteArrayInputStream +import java.io.InputStream +import kotlin.test.assertFailsWith import kotlinx.serialization.SerializationException import org.fdroid.index.IndexParser import org.fdroid.index.parseV2 @@ -11,204 +14,225 @@ import org.fdroid.test.TestDataMaxV2.PACKAGE_NAME_3 import org.fdroid.test.TestDataMaxV2.app3 import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 +import org.fdroid.test.TestUtils.getRes 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 + @Ignore("use for testing specific index on demand") + fun testBrokenIndexDiff() { + val endPath = "tmp/index-end.json" + val endIndex = IndexParser.parseV2(getRes(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 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 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 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 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 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 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 + @Test + fun testMinRemoveApp() { + val diffJson = + """ + { + "packages": { + "org.fdroid.min1": null + } } - } - } - }""".trimIndent() - testJsonDiff( - startPath = "index-min-v2.json", - diff = diffJson, - endIndex = TestDataMinV2.index.copy( - packages = TestDataMinV2.index.packages.mapValues { - it.value.copy(versions = emptyMap()) + """ + .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 testMinNoVersionsUnknownKey() { + val diffJson = + """ + { + "packages": { + "org.fdroid.min1": { + "metadata": { + "added": 42 + }, + "unknownKey": "should get ignored" + } } - ), - ) - } - - @Test - fun testMinRemoveMetadata() { - val diffJson = """{ - "packages": { - "org.fdroid.min1": { - "metadata": null + } + """ + .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)) } - }, - "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 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 - @Ignore("Removing all packages via diff currently not supported") // TODO - fun testMinRemovePackages() { - val diffJson = """{ - "packages": null - }""".trimIndent() - testJsonDiff( - startPath = "index-min-v2.json", - diff = diffJson, - endIndex = TestDataMinV2.index.copy( - packages = emptyMap(), - ), - ) - } + @Test + @Ignore("Removing all packages via diff currently not supported") // TODO + fun testMinRemovePackages() { + val diffJson = + """ + { + "packages": null + } + """ + .trimIndent() + testJsonDiff( + startPath = "index-min-v2.json", + diff = diffJson, + endIndex = TestDataMinV2.index.copy(packages = emptyMap()), + ) + } - @Test - fun testMinNoMetadataNoVersion() { - val diffJson = """{ - "packages": { - "org.fdroid.min1": { - } - } - }""".trimIndent() - testJsonDiff( - startPath = "index-min-v2.json", - diff = diffJson, - endIndex = TestDataMinV2.index, - ) - } + @Test + fun testMinNoMetadataNoVersion() { + val diffJson = + """ + { + "packages": { + "org.fdroid.min1": { + } + } + } + """ + .trimIndent() + testJsonDiff(startPath = "index-min-v2.json", diff = diffJson, endIndex = TestDataMinV2.index) + } - @Test - fun testMaxRemoveOptionals() { - val diffJson = """{ + @Test + fun testMaxRemoveOptionals() { + val diffJson = + """{ "packages": { "$PACKAGE_NAME_3": { "metadata": { @@ -247,111 +271,124 @@ internal class IndexV2DiffTest : DbTest() { } } } - }""".trimIndent() - val packages = TestDataMaxV2.index.packages.toMutableMap() - val versions = packages[PACKAGE_NAME_3]!!.versions.toMutableMap() - val version = versions["8c89ce2f42f4a89af8ca6e1ea220f9dfdee220724d8a9cc067d510ac6f3e0d06"]!! - versions["8c89ce2f42f4a89af8ca6e1ea220f9dfdee220724d8a9cc067d510ac6f3e0d06"] = version - .copy( - src = null, - releaseChannels = emptyList(), - antiFeatures = emptyMap(), - whatsNew = emptyMap(), - manifest = version.manifest.copy( - usesSdk = null, - maxSdkVersion = null, - signer = null, - usesPermission = emptyList(), - usesPermissionSdk23 = emptyList(), - nativecode = emptyList(), - features = emptyList(), - ) - ) - packages[PACKAGE_NAME_3] = app3.copy( - metadata = app3.metadata.copy( - name = null, - summary = null, - description = null, - categories = emptyList(), - donate = emptyList(), - icon = null, - featureGraphic = null, - promoGraphic = null, - tvBanner = null, - video = null, - screenshots = null, - ), - versions = versions, - ) - testJsonDiff( - startPath = "index-max-v2.json", - diff = diffJson, - endIndex = TestDataMaxV2.index.copy(packages = packages), - ) - } + }""" + .trimIndent() + val packages = TestDataMaxV2.index.packages.toMutableMap() + val versions = packages[PACKAGE_NAME_3]!!.versions.toMutableMap() + val version = versions["8c89ce2f42f4a89af8ca6e1ea220f9dfdee220724d8a9cc067d510ac6f3e0d06"]!! + versions["8c89ce2f42f4a89af8ca6e1ea220f9dfdee220724d8a9cc067d510ac6f3e0d06"] = + version.copy( + src = null, + releaseChannels = emptyList(), + antiFeatures = emptyMap(), + whatsNew = emptyMap(), + manifest = + version.manifest.copy( + usesSdk = null, + maxSdkVersion = null, + signer = null, + usesPermission = emptyList(), + usesPermissionSdk23 = emptyList(), + nativecode = emptyList(), + features = emptyList(), + ), + ) + packages[PACKAGE_NAME_3] = + app3.copy( + metadata = + app3.metadata.copy( + name = null, + summary = null, + description = null, + categories = emptyList(), + donate = emptyList(), + icon = null, + featureGraphic = null, + promoGraphic = null, + tvBanner = null, + video = null, + screenshots = null, + ), + versions = versions, + ) + testJsonDiff( + startPath = "index-max-v2.json", + diff = diffJson, + endIndex = TestDataMaxV2.index.copy(packages = packages), + ) + } - @Test - fun testAppDenyKeyList() { - val diffRepoIdJson = """{ - "packages": { - "org.fdroid.min1": { - "metadata": { - "repoId": 1 + @Test + fun testAppDenyKeyList() { + val diffRepoIdJson = + """ + { + "packages": { + "org.fdroid.min1": { + "metadata": { + "repoId": 1 + } + } + } } - } - } - }""".trimIndent() - assertFailsWith { - testJsonDiff( - startPath = "index-min-v2.json", - diff = diffRepoIdJson, - endIndex = TestDataMinV2.index, - ) - } - val diffPackageNameJson = """{ - "packages": { - "org.fdroid.min1": { - "metadata": { - "packageName": "foo" + """ + .trimIndent() + assertFailsWith { + testJsonDiff( + startPath = "index-min-v2.json", + diff = diffRepoIdJson, + endIndex = TestDataMinV2.index, + ) + } + val diffPackageNameJson = + """ + { + "packages": { + "org.fdroid.min1": { + "metadata": { + "packageName": "foo" + } + } + } } - } - } - }""".trimIndent() - assertFailsWith { - testJsonDiff( - startPath = "index-min-v2.json", - diff = diffPackageNameJson, - endIndex = TestDataMinV2.index, - ) - } + """ + .trimIndent() + assertFailsWith { + testJsonDiff( + startPath = "index-min-v2.json", + diff = diffPackageNameJson, + endIndex = TestDataMinV2.index, + ) } + } - @Test - fun testVersionsDenyKeyList() { - assertFailsWith { - testJsonDiff( - startPath = "index-min-v2.json", - diff = getMinVersionJson(""""packageName": "foo""""), - endIndex = TestDataMinV2.index, - ) - } - assertFailsWith { - testJsonDiff( - startPath = "index-min-v2.json", - diff = getMinVersionJson(""""repoId": 1"""), - endIndex = TestDataMinV2.index, - ) - } - assertFailsWith { - testJsonDiff( - startPath = "index-min-v2.json", - diff = getMinVersionJson(""""versionId": "bar""""), - endIndex = TestDataMinV2.index, - ) - } + @Test + fun testVersionsDenyKeyList() { + assertFailsWith { + testJsonDiff( + startPath = "index-min-v2.json", + diff = getMinVersionJson(""""packageName": "foo""""), + endIndex = TestDataMinV2.index, + ) } + assertFailsWith { + testJsonDiff( + startPath = "index-min-v2.json", + diff = getMinVersionJson(""""repoId": 1"""), + endIndex = TestDataMinV2.index, + ) + } + assertFailsWith { + testJsonDiff( + startPath = "index-min-v2.json", + diff = getMinVersionJson(""""versionId": "bar""""), + endIndex = TestDataMinV2.index, + ) + } + } - private fun getMinVersionJson(insert: String) = """{ + private fun getMinVersionJson(insert: String) = + """{ "packages": { "org.fdroid.min1": { "versions": { @@ -360,87 +397,97 @@ internal class IndexV2DiffTest : DbTest() { } } } - }""".trimIndent() + }""" + .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.PACKAGE_NAME_1 to TestDataMidV2.app1, - TestDataMidV2.PACKAGE_NAME_2 to fdroidPackage, - ) - ), - ) - } - - @Test - fun testMinAddChinese() { - val diffJson = """{ - "packages": { - "org.fdroid.min1": { - "metadata": { - "name": { "zh-CN": "自由软件仓库" }, - "summary": { "ja": "这个仓库中的" }, - "description": { "ko-KR": "切始终是从" } - } - } - } - }""".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( - // zero whitespaces (to separate tokens) will be added in testJsonDiff() - name = mapOf("zh-CN" to "自由软件仓库"), - summary = mapOf("ja" to "这个仓库中的"), - description = mapOf("ko-KR" to "切始终是从"), - ) - ) + @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.PACKAGE_NAME_1 to TestDataMidV2.app1, + TestDataMidV2.PACKAGE_NAME_2 to fdroidPackage, + ) + ), + ) + } - private fun testJsonDiff(startPath: String, diff: String, endIndex: IndexV2) { - testDiff(startPath, ByteArrayInputStream(diff.toByteArray()), endIndex) - } + @Test + fun testMinAddChinese() { + val diffJson = + """ + { + "packages": { + "org.fdroid.min1": { + "metadata": { + "name": { "zh-CN": "自由软件仓库" }, + "summary": { "ja": "这个仓库中的" }, + "description": { "ko-KR": "切始终是从" } + } + } + } + } + """ + .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( + // zero whitespaces (to separate tokens) will be added in testJsonDiff() + name = mapOf("zh-CN" to "自由软件仓库"), + summary = mapOf("ja" to "这个仓库中的"), + description = mapOf("ko-KR" to "切始终是从"), + ) + ) + } + ), + ) + } - private fun testDiff(startPath: String, diffPath: String, endIndex: IndexV2) { - testDiff(startPath, assets.open(diffPath), endIndex) - } + private fun testJsonDiff(startPath: String, diff: String, endIndex: IndexV2) { + testDiff(startPath, ByteArrayInputStream(diff.toByteArray()), endIndex) + } - private fun testDiff(startPath: String, diffStream: InputStream, endIndex: IndexV2) { - // stream start index into the DB - val repoId = streamIndexV2IntoDb(startPath) + private fun testDiff(startPath: String, diffPath: String, endIndex: IndexV2) { + testDiff(startPath, getRes(diffPath), endIndex) + } - // 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) - } + 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) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2InsertTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2InsertTest.kt index fffc872f6..0a4df0628 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/IndexV2InsertTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/IndexV2InsertTest.kt @@ -1,6 +1,9 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue import kotlinx.serialization.SerializationException import org.apache.commons.io.input.CountingInputStream import org.fdroid.CompatibilityChecker @@ -9,73 +12,68 @@ import org.fdroid.test.TestDataEmptyV2 import org.fdroid.test.TestDataMaxV2 import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 +import org.fdroid.test.TestUtils.getRes 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 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 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 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 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 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 { - 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) + @Test + fun testExceptionWhileStreamingDoesNotSaveIntoDb() { + val cIn = CountingInputStream(getRes("index-max-v2.json")) + val compatibilityChecker = CompatibilityChecker { + if (cIn.byteCount > 0) throw SerializationException() + true } - + assertFailsWith { + 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) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt index 0a4f171ff..6a6c47113 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/MultiRepoMigrationTest.kt @@ -8,313 +8,367 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import org.fdroid.database.Converters.localizedTextV2toString -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.fail +import org.fdroid.database.Converters.localizedTextV2toString +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith private const val TEST_DB = "migration-test" @RunWith(AndroidJUnit4::class) internal class MultiRepoMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - FDroidDatabaseInt::class.java, - listOf(MultiRepoMigration()), - FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + FDroidDatabaseInt::class.java, + listOf(MultiRepoMigration()), + FrameworkSQLiteOpenHelperFactory(), ) - private val fdroidArchiveRepo = InitialRepository( - name = "F-Droid Archive", - address = "https://f-droid.org/archive", - description = "The archive repository of the F-Droid client. " + - "This contains older versions of\n" + - "applications from the main repository.", - certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", - version = 13L, - enabled = false, - weight = 0, // gets set later + private val fdroidArchiveRepo = + InitialRepository( + name = "F-Droid Archive", + address = "https://f-droid.org/archive", + description = + "The archive repository of the F-Droid client. " + + "This contains older versions of\n" + + "applications from the main repository.", + certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", + version = 13L, + enabled = false, + weight = 0, // gets set later ) - private val fdroidRepo = InitialRepository( - name = "F-Droid", - address = "https://f-droid.org/repo", - description = "The official F-Droid Free Software repository. " + - "Everything in this repository is always built from the source code.", - certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", - version = 13L, - enabled = true, - weight = 0, // gets set later + private val fdroidRepo = + InitialRepository( + name = "F-Droid", + address = "https://f-droid.org/repo", + description = + "The official F-Droid Free Software repository. " + + "Everything in this repository is always built from the source code.", + certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105", + version = 13L, + enabled = true, + weight = 0, // gets set later ) - private val guardianArchiveRepo = InitialRepository( - name = "Guardian Project Archive", - address = "https://guardianproject.info/fdroid/archive", - description = "The official repository of The Guardian Project apps" + - " for use with F-Droid client. This\n" + - " contains older versions of applications from the main repository.\n", - certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105", - version = 13L, - enabled = false, - weight = 0, // gets set later + private val guardianArchiveRepo = + InitialRepository( + name = "Guardian Project Archive", + address = "https://guardianproject.info/fdroid/archive", + description = + "The official repository of The Guardian Project apps" + + " for use with F-Droid client. This\n" + + " contains older versions of applications from the main repository.\n", + certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105", + version = 13L, + enabled = false, + weight = 0, // gets set later ) - private val guardianRepo = InitialRepository( - name = "Guardian Project", - address = "https://guardianproject.info/fdroid/repo", - description = "The official app repository of The Guardian Project. " + - "Applications in this repository\n" + - " are official binaries build by the original application developers " + - "and signed by the\n" + - " same key as the APKs that are released in the Google Play store.", - certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105", - version = 13L, - enabled = false, - weight = 0, // gets set later + private val guardianRepo = + InitialRepository( + name = "Guardian Project", + address = "https://guardianproject.info/fdroid/repo", + description = + "The official app repository of The Guardian Project. " + + "Applications in this repository\n" + + " are official binaries build by the original application developers " + + "and signed by the\n" + + " same key as the APKs that are released in the Google Play store.", + certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105", + version = 13L, + enabled = false, + weight = 0, // gets set later ) - @Test - fun migrateDefaultRepos() { - val reposToMigrate = listOf( - fdroidArchiveRepo.copy(weight = 1), - fdroidRepo.copy(weight = 2), + @Test + fun migrateDefaultRepos() { + val reposToMigrate = listOf(fdroidArchiveRepo.copy(weight = 1), fdroidRepo.copy(weight = 2)) + runRepoMigration(reposToMigrate) { db -> + db + .getRepositoryDao() + .getRepositories() + .sortedByDescending { it.weight } + .also { repos -> + assertEquals(reposToMigrate.size, repos.size) + assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) + assertEquals(fdroidRepo.address, repos[0].address) + assertEquals(1_000_000_000, repos[0].weight) + assertEquals(fdroidArchiveRepo.address, repos[1].address) + assertEquals(999_999_999, repos[1].weight) + } + } + } + + @Test + fun migrateOldDefaultRepos() { + val reposToMigrate = + listOf( + fdroidArchiveRepo.copy(weight = 1), + fdroidRepo.copy(weight = 2), + guardianArchiveRepo.copy(weight = 3), + guardianRepo.copy(weight = 4), + ) + runRepoMigration(reposToMigrate) { db -> + db + .getRepositoryDao() + .getRepositories() + .sortedByDescending { it.weight } + .also { repos -> + assertEquals(reposToMigrate.size, repos.size) + assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) + assertEquals(guardianRepo.address, repos[0].address) + assertEquals(1_000_000_000, repos[0].weight) + assertEquals(guardianArchiveRepo.address, repos[1].address) + assertEquals(999_999_999, repos[1].weight) + assertEquals(fdroidRepo.address, repos[2].address) + assertEquals(999_999_998, repos[2].weight) + assertEquals(fdroidArchiveRepo.address, repos[3].address) + assertEquals(999_999_997, repos[3].weight) + } + } + } + + @Test + fun migrateOldDefaultReposPlusRandomOnes() { + val reposToMigrate = + listOf( + fdroidArchiveRepo.copy(weight = 1), + fdroidRepo.copy(weight = 2), + guardianArchiveRepo.copy(weight = 3), + guardianRepo.copy(weight = 4), + InitialRepository( + name = "Foo bar", + address = "https://example.org/fdroid/repo", + description = "foo bar repo", + certificate = "1234567890", + version = 0L, + enabled = true, + weight = 5, + ), + InitialRepository( + name = "Bla Blub", + address = "https://example.com/fdroid/repo", + description = "bla blub repo", + certificate = "0987654321", + version = 0L, + enabled = true, + weight = 6, + ), + ) + runRepoMigration(reposToMigrate) { db -> + db + .getRepositoryDao() + .getRepositories() + .sortedByDescending { it.weight } + .also { repos -> + assertEquals(reposToMigrate.size, repos.size) + assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) + assertEquals("https://example.com/fdroid/repo", repos[0].address) + assertEquals(1_000_000_000, repos[0].weight) + assertEquals("https://example.org/fdroid/repo", repos[1].address) + assertEquals(999_999_998, repos[1].weight) // space for archive above + assertEquals(guardianRepo.address, repos[2].address) + assertEquals(999_999_996, repos[2].weight) + assertEquals(guardianArchiveRepo.address, repos[3].address) + assertEquals(999_999_995, repos[3].weight) + assertEquals(fdroidRepo.address, repos[4].address) + assertEquals(999_999_994, repos[4].weight) + assertEquals(fdroidArchiveRepo.address, repos[5].address) + assertEquals(999_999_993, repos[5].weight) + } + } + } + + @Test + fun migrateArchiveWithoutMainRepo() { + val reposToMigrate = + listOf( + InitialRepository( + name = "Foo bar", + address = "https://example.org/fdroid/repo", + description = "foo bar repo", + certificate = "1234567890", + version = 0L, + enabled = true, + weight = 2, + ), + fdroidArchiveRepo.copy(weight = 5), + guardianRepo.copy(weight = 6), + ) + runRepoMigration(reposToMigrate) { db -> + db + .getRepositoryDao() + .getRepositories() + .sortedByDescending { it.weight } + .also { repos -> + assertEquals(reposToMigrate.size, repos.size) + assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) + assertEquals(guardianRepo.address, repos[0].address) + assertEquals(1_000_000_000, repos[0].weight) // space for archive below + assertEquals("https://example.org/fdroid/repo", repos[1].address) + assertEquals(999_999_998, repos[1].weight) + assertEquals(fdroidArchiveRepo.address, repos[2].address) // archive at the end + assertEquals(999_999_996, repos[2].weight) + } + } + } + + @Test + fun testPreferredRepoChanges() { + var repoId: Long + val packageName = "org.example" + helper.createDatabase(TEST_DB, 1).use { db -> + // Database has schema version 1. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + val repo = fdroidRepo + repoId = + db.insert( + CoreRepository.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to repo.name))) + put("description", localizedTextV2toString(mapOf("en-US" to repo.description))) + put("address", repo.address) + put("timestamp", -1) + put("certificate", repo.certificate) + }, ) - runRepoMigration(reposToMigrate) { db -> - db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos -> - assertEquals(reposToMigrate.size, repos.size) - assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) - assertEquals(fdroidRepo.address, repos[0].address) - assertEquals(1_000_000_000, repos[0].weight) - assertEquals(fdroidArchiveRepo.address, repos[1].address) - assertEquals(999_999_999, repos[1].weight) - } - } + db.insert( + RepositoryPreferences.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("enabled", repo.enabled) + put("weight", repo.weight) + }, + ) + // insert an app with empty app prefs + db.insert( + AppMetadata.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("packageName", packageName) + put("added", 23L) + put("lastUpdated", 42L) + put("isCompatible", true) + }, + ) + db.insert( + AppPrefs.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("packageName", packageName) + put("ignoreVersionCodeUpdate", 0) + }, + ) } - @Test - fun migrateOldDefaultRepos() { - val reposToMigrate = listOf( - fdroidArchiveRepo.copy(weight = 1), - fdroidRepo.copy(weight = 2), - guardianArchiveRepo.copy(weight = 3), - guardianRepo.copy(weight = 4), + // Re-open the database with version 2, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 2, true).close() + + // now get the Room DB, so we can use our DAOs for verifying the migration + databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> + // migrated apps have no preferred repo set + assertNotNull(db.getAppDao().getApp(repoId, packageName)) + val appPrefs = db.getAppPrefsDao().getAppPrefsOrNull(packageName) ?: fail() + assertEquals(packageName, appPrefs.packageName) + assertNull(appPrefs.preferredRepoId) + + // preferred repo inferred from repo priorities + val preferredRepos = db.getAppPrefsDao().getPreferredRepos(listOf(packageName)) + assertEquals(1, preferredRepos.size) + assertEquals(repoId, preferredRepos[packageName]) + } + } + + @Test + fun repoWithoutCertificate() { + helper.createDatabase(TEST_DB, 1).use { db -> + // Database has schema version 1. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + val repoId = + db.insert( + CoreRepository.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to fdroidRepo.name))) + put("description", localizedTextV2toString(mapOf("en-US" to fdroidRepo.description))) + put("address", fdroidRepo.address) + put("timestamp", -1) + }, ) - runRepoMigration(reposToMigrate) { db -> - db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos -> - assertEquals(reposToMigrate.size, repos.size) - assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) - assertEquals(guardianRepo.address, repos[0].address) - assertEquals(1_000_000_000, repos[0].weight) - assertEquals(guardianArchiveRepo.address, repos[1].address) - assertEquals(999_999_999, repos[1].weight) - assertEquals(fdroidRepo.address, repos[2].address) - assertEquals(999_999_998, repos[2].weight) - assertEquals(fdroidArchiveRepo.address, repos[3].address) - assertEquals(999_999_997, repos[3].weight) - } - } + db.insert( + RepositoryPreferences.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("enabled", fdroidRepo.enabled) + put("weight", fdroidRepo.weight) + }, + ) } - @Test - fun migrateOldDefaultReposPlusRandomOnes() { - val reposToMigrate = listOf( - fdroidArchiveRepo.copy(weight = 1), - fdroidRepo.copy(weight = 2), - guardianArchiveRepo.copy(weight = 3), - guardianRepo.copy(weight = 4), - InitialRepository( - name = "Foo bar", - address = "https://example.org/fdroid/repo", - description = "foo bar repo", - certificate = "1234567890", - version = 0L, - enabled = true, - weight = 5, - ), - InitialRepository( - name = "Bla Blub", - address = "https://example.com/fdroid/repo", - description = "bla blub repo", - certificate = "0987654321", - version = 0L, - enabled = true, - weight = 6, - ), + // Re-open the database with version 2, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 2, true).close() + + // now get the Room DB, so we can use our DAOs for verifying the migration + databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> + // repo without cert did not get migrated, because we auto-migrate to latest version + assertEquals(0, db.getRepositoryDao().getRepositories().size) + } + } + + private fun runRepoMigration(repos: List, check: (FDroidDatabaseInt) -> Unit) { + helper.createDatabase(TEST_DB, 1).use { db -> + // Database has schema version 1. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + repos.forEach { repo -> + val repoId = + db.insert( + CoreRepository.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to repo.name))) + put("description", localizedTextV2toString(mapOf("en-US" to repo.description))) + put("address", repo.address) + put("timestamp", -1) + put("certificate", repo.certificate) + }, + ) + db.insert( + RepositoryPreferences.TABLE, + CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("enabled", repo.enabled) + put("weight", repo.weight) + }, ) - runRepoMigration(reposToMigrate) { db -> - db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos -> - assertEquals(reposToMigrate.size, repos.size) - assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) - assertEquals("https://example.com/fdroid/repo", repos[0].address) - assertEquals(1_000_000_000, repos[0].weight) - assertEquals("https://example.org/fdroid/repo", repos[1].address) - assertEquals(999_999_998, repos[1].weight) // space for archive above - assertEquals(guardianRepo.address, repos[2].address) - assertEquals(999_999_996, repos[2].weight) - assertEquals(guardianArchiveRepo.address, repos[3].address) - assertEquals(999_999_995, repos[3].weight) - assertEquals(fdroidRepo.address, repos[4].address) - assertEquals(999_999_994, repos[4].weight) - assertEquals(fdroidArchiveRepo.address, repos[5].address) - assertEquals(999_999_993, repos[5].weight) - } - } + } } - @Test - fun migrateArchiveWithoutMainRepo() { - val reposToMigrate = listOf( - InitialRepository( - name = "Foo bar", - address = "https://example.org/fdroid/repo", - description = "foo bar repo", - certificate = "1234567890", - version = 0L, - enabled = true, - weight = 2, - ), - fdroidArchiveRepo.copy(weight = 5), - guardianRepo.copy(weight = 6), - ) - runRepoMigration(reposToMigrate) { db -> - db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos -> - assertEquals(reposToMigrate.size, repos.size) - assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size) - assertEquals(guardianRepo.address, repos[0].address) - assertEquals(1_000_000_000, repos[0].weight) // space for archive below - assertEquals("https://example.org/fdroid/repo", repos[1].address) - assertEquals(999_999_998, repos[1].weight) - assertEquals(fdroidArchiveRepo.address, repos[2].address) // archive at the end - assertEquals(999_999_996, repos[2].weight) - } - } - } + // Re-open the database with version 2, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 2, true).close() - @Test - fun testPreferredRepoChanges() { - var repoId: Long - val packageName = "org.example" - helper.createDatabase(TEST_DB, 1).use { db -> - // Database has schema version 1. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - val repo = fdroidRepo - repoId = db.insert(CoreRepository.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to repo.name))) - put("description", localizedTextV2toString(mapOf("en-US" to repo.description))) - put("address", repo.address) - put("timestamp", -1) - put("certificate", repo.certificate) - }) - db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("enabled", repo.enabled) - put("weight", repo.weight) - }) - // insert an app with empty app prefs - db.insert(AppMetadata.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("packageName", packageName) - put("added", 23L) - put("lastUpdated", 42L) - put("isCompatible", true) - }) - db.insert(AppPrefs.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("packageName", packageName) - put("ignoreVersionCodeUpdate", 0) - }) - } - - // Re-open the database with version 2, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 2, true).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build() - .use { db -> - // migrated apps have no preferred repo set - assertNotNull(db.getAppDao().getApp(repoId, packageName)) - val appPrefs = db.getAppPrefsDao().getAppPrefsOrNull(packageName) ?: fail() - assertEquals(packageName, appPrefs.packageName) - assertNull(appPrefs.preferredRepoId) - - // preferred repo inferred from repo priorities - val preferredRepos = db.getAppPrefsDao().getPreferredRepos(listOf(packageName)) - assertEquals(1, preferredRepos.size) - assertEquals(repoId, preferredRepos[packageName]) - } - } - - @Test - fun repoWithoutCertificate() { - helper.createDatabase(TEST_DB, 1).use { db -> - // Database has schema version 1. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - val repoId = db.insert(CoreRepository.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to fdroidRepo.name))) - put( - "description", - localizedTextV2toString(mapOf("en-US" to fdroidRepo.description)) - ) - put("address", fdroidRepo.address) - put("timestamp", -1) - }) - db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("enabled", fdroidRepo.enabled) - put("weight", fdroidRepo.weight) - }) - } - - // Re-open the database with version 2, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 2, true).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build().use { db -> - // repo without cert did not get migrated, because we auto-migrate to latest version - assertEquals(0, db.getRepositoryDao().getRepositories().size) - } - } - - private fun runRepoMigration( - repos: List, - check: (FDroidDatabaseInt) -> Unit, - ) { - helper.createDatabase(TEST_DB, 1).use { db -> - // Database has schema version 1. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - repos.forEach { repo -> - val repoId = db.insert(CoreRepository.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to repo.name))) - put("description", localizedTextV2toString(mapOf("en-US" to repo.description))) - put("address", repo.address) - put("timestamp", -1) - put("certificate", repo.certificate) - }) - db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, ContentValues().apply { - put("repoId", repoId) - put("enabled", repo.enabled) - put("weight", repo.weight) - }) - } - } - - // Re-open the database with version 2, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 2, true).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build().use { db -> - check(db) - } - } + // now get the Room DB, so we can use our DAOs for verifying the migration + databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> check(db) } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt index 8a30dbd57..cb7d8134b 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/PreferredRepoMigrationTest.kt @@ -8,92 +8,96 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import kotlin.random.Random +import kotlin.test.assertEquals import org.fdroid.database.Converters.localizedTextV2toString import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import kotlin.random.Random -import kotlin.test.assertEquals private const val TEST_DB = "migration-test" @RunWith(AndroidJUnit4::class) internal class PreferredRepoMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - instrumentation = InstrumentationRegistry.getInstrumentation(), - databaseClass = FDroidDatabaseInt::class.java, - specs = emptyList(), - openFactory = FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + instrumentation = InstrumentationRegistry.getInstrumentation(), + databaseClass = FDroidDatabaseInt::class.java, + specs = emptyList(), + openFactory = FrameworkSQLiteOpenHelperFactory(), ) - @Test - fun migrateRepos() { - helper.createDatabase(TEST_DB, 6).use { db -> - // Database has schema version 6. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - val repoId = db.insert( - CoreRepository.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to "foo"))) - put("description", localizedTextV2toString(mapOf("en-US" to "bar"))) - put("address", "https://example.org/repo") - put("certificate", "0123") - put("timestamp", -1) - }, - ) - db.insert( - RepositoryPreferences.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("repoId", repoId) - put("enabled", true) - put("weight", Long.MAX_VALUE) - }, - ) - val oeffiMetadata = ContentValues().apply { - put("packageName", "de.schildbach.oeffi") - put("repoId", repoId) - put("localizedName", "Öffi") - put("localizedSummary", "Der König des Fahrplandschungels!") - put("added", Random.nextLong()) - put("lastUpdated", Random.nextLong()) - put("isCompatible", true) - } - db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiMetadata) - val oeffiPrefs = ContentValues().apply { - put("packageName", "de.schildbach.oeffi") - put("ignoreVersionCodeUpdate", 0) - put("preferredRepoId", 42) // repo doesn't exist - } - db.insert(AppPrefs.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiPrefs) - } - - // Re-open the database with version 2, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 7, true).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - Room.databaseBuilder( - ApplicationProvider.getApplicationContext(), - FDroidDatabaseInt::class.java, - TEST_DB + @Test + fun migrateRepos() { + helper.createDatabase(TEST_DB, 6).use { db -> + // Database has schema version 6. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + val repoId = + db.insert( + CoreRepository.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to "foo"))) + put("description", localizedTextV2toString(mapOf("en-US" to "bar"))) + put("address", "https://example.org/repo") + put("certificate", "0123") + put("timestamp", -1) + }, ) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build().use { db -> - // repo without cert did not get migrated, the other one did - assertEquals(1, db.getRepositoryDao().getRepositories().size) - val repo = db.getRepositoryDao().getRepositories()[0] - assertEquals("https://example.org/repo", repo.address) - - db.getAppPrefsDao().getPreferredRepos(listOf("de.schildbach.oeffi")).let { - assertEquals(1, it.size) - // now correct PreferredRepo gets returned - assertEquals(repo.repoId, it["de.schildbach.oeffi"]) - } - } + db.insert( + RepositoryPreferences.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId) + put("enabled", true) + put("weight", Long.MAX_VALUE) + }, + ) + val oeffiMetadata = + ContentValues().apply { + put("packageName", "de.schildbach.oeffi") + put("repoId", repoId) + put("localizedName", "Öffi") + put("localizedSummary", "Der König des Fahrplandschungels!") + put("added", Random.nextLong()) + put("lastUpdated", Random.nextLong()) + put("isCompatible", true) + } + db.insert(AppMetadata.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiMetadata) + val oeffiPrefs = + ContentValues().apply { + put("packageName", "de.schildbach.oeffi") + put("ignoreVersionCodeUpdate", 0) + put("preferredRepoId", 42) // repo doesn't exist + } + db.insert(AppPrefs.TABLE, SQLiteDatabase.CONFLICT_FAIL, oeffiPrefs) } + // Re-open the database with version 2, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 7, true).close() + + // now get the Room DB, so we can use our DAOs for verifying the migration + Room.databaseBuilder( + ApplicationProvider.getApplicationContext(), + FDroidDatabaseInt::class.java, + TEST_DB, + ) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> + // repo without cert did not get migrated, the other one did + assertEquals(1, db.getRepositoryDao().getRepositories().size) + val repo = db.getRepositoryDao().getRepositories()[0] + assertEquals("https://example.org/repo", repo.address) + + db.getAppPrefsDao().getPreferredRepos(listOf("de.schildbach.oeffi")).let { + assertEquals(1, it.size) + // now correct PreferredRepo gets returned + assertEquals(repo.repoId, it["de.schildbach.oeffi"]) + } + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt index 167210a88..c1ecc3aee 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/RepoCertNonNullMigrationTest.kt @@ -8,84 +8,87 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import kotlin.test.assertEquals import org.fdroid.database.Converters.localizedTextV2toString import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals private const val TEST_DB = "migration-test" @RunWith(AndroidJUnit4::class) internal class RepoCertNonNullMigrationTest { - @get:Rule - val helper: MigrationTestHelper = MigrationTestHelper( - instrumentation = getInstrumentation(), - databaseClass = FDroidDatabaseInt::class.java, - specs = emptyList(), - openFactory = FrameworkSQLiteOpenHelperFactory(), + @get:Rule + val helper: MigrationTestHelper = + MigrationTestHelper( + instrumentation = getInstrumentation(), + databaseClass = FDroidDatabaseInt::class.java, + specs = emptyList(), + openFactory = FrameworkSQLiteOpenHelperFactory(), ) - @Test - fun migrateRepos() { - helper.createDatabase(TEST_DB, 2).use { db -> - // Database has schema version 2. Insert some data using SQL queries. - // We can't use DAO classes because they expect the latest schema. - val repoId1 = db.insert( - CoreRepository.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to "foo"))) - put("description", localizedTextV2toString(mapOf("en-US" to "bar"))) - put("address", "https://example.org/repo") - put("certificate", "0123") - put("timestamp", -1) - }, - ) - db.insert( - RepositoryPreferences.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("repoId", repoId1) - put("enabled", true) - put("weight", Long.MAX_VALUE) - }, - ) - val repoId2 = db.insert( - CoreRepository.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("name", localizedTextV2toString(mapOf("en-US" to "no cert"))) - put("description", localizedTextV2toString(mapOf("en-US" to "no cert desc"))) - put("address", "https://example.com/repo") - put("timestamp", -1) - }, - ) - db.insert( - RepositoryPreferences.TABLE, - SQLiteDatabase.CONFLICT_FAIL, - ContentValues().apply { - put("repoId", repoId2) - put("enabled", true) - put("weight", Long.MAX_VALUE - 2) - }, - ) - } - - // Re-open the database with version 2, auto-migrations are applied automatically - helper.runMigrationsAndValidate(TEST_DB, 4, true, MIGRATION_2_3).close() - - // now get the Room DB, so we can use our DAOs for verifying the migration - Room.databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) - .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) - .allowMainThreadQueries() - .build().use { db -> - // repo without cert did not get migrated, the other one did - assertEquals(1, db.getRepositoryDao().getRepositories().size) - val repo = db.getRepositoryDao().getRepositories()[0] - assertEquals("https://example.org/repo", repo.address) - } + @Test + fun migrateRepos() { + helper.createDatabase(TEST_DB, 2).use { db -> + // Database has schema version 2. Insert some data using SQL queries. + // We can't use DAO classes because they expect the latest schema. + val repoId1 = + db.insert( + CoreRepository.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to "foo"))) + put("description", localizedTextV2toString(mapOf("en-US" to "bar"))) + put("address", "https://example.org/repo") + put("certificate", "0123") + put("timestamp", -1) + }, + ) + db.insert( + RepositoryPreferences.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId1) + put("enabled", true) + put("weight", Long.MAX_VALUE) + }, + ) + val repoId2 = + db.insert( + CoreRepository.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", localizedTextV2toString(mapOf("en-US" to "no cert"))) + put("description", localizedTextV2toString(mapOf("en-US" to "no cert desc"))) + put("address", "https://example.com/repo") + put("timestamp", -1) + }, + ) + db.insert( + RepositoryPreferences.TABLE, + SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("repoId", repoId2) + put("enabled", true) + put("weight", Long.MAX_VALUE - 2) + }, + ) } + // Re-open the database with version 2, auto-migrations are applied automatically + helper.runMigrationsAndValidate(TEST_DB, 4, true, MIGRATION_2_3).close() + + // now get the Room DB, so we can use our DAOs for verifying the migration + Room.databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB) + .addMigrations(MIGRATION_2_3, MIGRATION_5_6, MIGRATION_8_9) + .allowMainThreadQueries() + .build() + .use { db -> + // repo without cert did not get migrated, the other one did + assertEquals(1, db.getRepositoryDao().getRepositories().size) + val repo = db.getRepositoryDao().getRepositories()[0] + assertEquals("https://example.org/repo", repo.address) + } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt index 1a1f079c5..71bb2c3cf 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt @@ -1,6 +1,13 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail import org.fdroid.database.TestUtils.assertRepoEquals import org.fdroid.database.TestUtils.getOrFail import org.fdroid.test.TestAppUtils.getRandomMetadataV2 @@ -10,372 +17,365 @@ import org.fdroid.test.TestUtils.orNull 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.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail @RunWith(AndroidJUnit4::class) internal class RepositoryDaoTest : DbTest() { - @Test - fun testInsertInitialRepository() { - val repo = InitialRepository( - name = getRandomString(), - address = getRandomString(), - mirrors = listOf(getRandomString(), getRandomString(), getRandomString()), - description = getRandomString(), - certificate = "abcdef", // not random, because format gets checked - version = Random.nextLong(), - enabled = Random.nextBoolean(), - ) - val repoId = repoDao.insert(repo) + @Test + fun testInsertInitialRepository() { + val repo = + InitialRepository( + name = getRandomString(), + address = getRandomString(), + mirrors = listOf(getRandomString(), getRandomString(), getRandomString()), + description = getRandomString(), + certificate = "abcdef", // not random, because format gets checked + version = Random.nextLong(), + enabled = Random.nextBoolean(), + ) + 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(Int.MAX_VALUE - 2, actualRepo.weight) // ignoring provided weight - assertEquals(-1, actualRepo.timestamp) - assertEquals(3, actualRepo.mirrors.size) - assertEquals(emptyList(), actualRepo.userMirrors) - assertEquals(emptyList(), actualRepo.disabledMirrors) - assertEquals(repo.mirrors.toSet(), actualRepo.mirrors.map { it.url }.toSet()) - 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) + 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(Int.MAX_VALUE - 2, actualRepo.weight) // ignoring provided weight + assertEquals(-1, actualRepo.timestamp) + assertEquals(3, actualRepo.mirrors.size) + assertEquals(emptyList(), actualRepo.userMirrors) + assertEquals(emptyList(), actualRepo.disabledMirrors) + assertEquals(repo.mirrors.toSet(), actualRepo.mirrors.map { it.url }.toSet()) + 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) - @Test - fun testInsertEmptyRepo() { - // insert empty repo - val address = getRandomString() - val username = getRandomString().orNull() - val password = getRandomString().orNull() - val repoId = repoDao.insertEmptyRepo(address, username, password) + // insert second repo + val repo2 = getRandomRepo() + val repoId2 = repoDao.insertOrReplace(repo2) - // 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) + // 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?.minus(2), 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)) - @Test - fun insertAndDeleteTwoRepos() { - // insert first repo - val repo1 = getRandomRepo() - val repoId1 = repoDao.insertOrReplace(repo1) + // 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)) + } - // 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) + @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) - // insert second repo - val repo2 = getRandomRepo() - val repoId2 = repoDao.insertOrReplace(repo2) + repoDao.clearAll() + assertEquals(0, repoDao.getRepositories().size) + assertEquals(0, repoDao.getLiveRepositories().getOrFail().size) + } - // 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?.minus(2), repositoryPreferences2?.weight) + @Test + fun testGetRepositoryByCert() { + val cert = getRandomString() + // insert repo and (required) preferences + val repo1 = getRandomRepo().toCoreRepository(version = 42L, certificate = cert) + val repoId = repoDao.insertOrReplace(repo1) + val repositoryPreferences = RepositoryPreferences(repoId, 3) + repoDao.insert(repositoryPreferences) - // 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)) + // repo is returned when querying for right cert + assertEquals(repo1.copy(repoId = repoId), repoDao.getRepository(cert)?.repository) + // nothing is returned when querying for non-existent cert + assertNull(repoDao.getRepository("foo bar")) + } - // 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 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).getOrFail().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).getOrFail().size) + assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) + // preferences are not touched by clearing + assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId)) + } + + @Test + fun testGetMinRepositoryWeight() { + assertEquals(Int.MAX_VALUE, repoDao.getMinRepositoryWeight()) + + repoDao.insertOrReplace(getRandomRepo()) + assertEquals(Int.MAX_VALUE - 2, repoDao.getMinRepositoryWeight()) + + repoDao.insertOrReplace(getRandomRepo()) + assertEquals(Int.MAX_VALUE - 4, repoDao.getMinRepositoryWeight()) + } + + @Test + fun testReorderRepositories() { + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId4 = repoDao.insertOrReplace(getRandomRepo()) + val repoId5 = repoDao.insertOrReplace(getRandomRepo()) + + // repos are listed in the order they entered the DB [1, 2, 3, 4, 5] + assertEquals( + listOf(repoId1, repoId2, repoId3, repoId4, repoId5), + repoDao.getRepositories().map { it.repoId }, + ) + + // 2 gets moved to 5 [1, 3, 4, 5, 2] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId2) ?: fail(), + repoTarget = repoDao.getRepository(repoId5) ?: fail(), + ) + assertEquals( + listOf(repoId1, repoId3, repoId4, repoId5, repoId2), + repoDao.getRepositories().map { it.repoId }, + ) + + // 5 gets moved to 1 [5, 1, 3, 4, 2] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId5) ?: fail(), + repoTarget = repoDao.getRepository(repoId1) ?: fail(), + ) + assertEquals( + listOf(repoId5, repoId1, repoId3, repoId4, repoId2), + repoDao.getRepositories().map { it.repoId }, + ) + + // 3 gets moved to 5 [3, 5, 1, 4, 2] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId3) ?: fail(), + repoTarget = repoDao.getRepository(repoId5) ?: fail(), + ) + assertEquals( + listOf(repoId3, repoId5, repoId1, repoId4, repoId2), + repoDao.getRepositories().map { it.repoId }, + ) + + // 3 gets moved to itself, list shouldn't change [3, 5, 1, 4, 2] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId3) ?: fail(), + repoTarget = repoDao.getRepository(repoId3) ?: fail(), + ) + assertEquals( + listOf(repoId3, repoId5, repoId1, repoId4, repoId2), + repoDao.getRepositories().map { it.repoId }, + ) + + // we'll add an archive repo for repo1 to the list [3, 5, (1, 1a), 4, 2] + val repo1 = repoDao.getRepository(repoId1) ?: fail() + repoDao.updateRepository(repo1.repository.copy(certificate = "1234abcd")) + val repo1a = + InitialRepository( + name = getRandomString(), + address = "https://example.org/archive", + description = getRandomString(), + certificate = "1234abcd", // same as repo1 + version = 42L, + enabled = false, + ) + val repoId1a = repoDao.insert(repo1a) + repoDao.setWeight(repoId1a, repo1.weight - 1) + + // now we move repo 1 to position of repo 2 [3, 5, 4, 2, (1, 1a)] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId1) ?: fail(), + repoTarget = repoDao.getRepository(repoId2) ?: fail(), + ) + assertEquals( + listOf(repoId3, repoId5, repoId4, repoId2, repoId1, repoId1a), + repoDao.getRepositories().map { it.repoId }, + ) + + // now move repo 1 and its archive to top position [(1, 1a), 3, 5, 4, 2] + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId1) ?: fail(), + repoTarget = repoDao.getRepository(repoId3) ?: fail(), + ) + assertEquals( + listOf(repoId1, repoId1a, repoId3, repoId5, repoId4, repoId2), + repoDao.getRepositories().map { it.repoId }, + ) + + // archive repos can't be reordered directly + assertFailsWith { + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId1a) ?: fail(), + repoTarget = repoDao.getRepository(repoId3) ?: fail(), + ) } - - @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 testGetRepositoryByCert() { - val cert = getRandomString() - // insert repo and (required) preferences - val repo1 = getRandomRepo().toCoreRepository(version = 42L, certificate = cert) - val repoId = repoDao.insertOrReplace(repo1) - val repositoryPreferences = RepositoryPreferences(repoId, 3) - repoDao.insert(repositoryPreferences) - - // repo is returned when querying for right cert - assertEquals(repo1.copy(repoId = repoId), repoDao.getRepository(cert)?.repository) - // nothing is returned when querying for non-existent cert - assertNull(repoDao.getRepository("foo bar")) - } - - @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).getOrFail().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).getOrFail().size) - assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) - // preferences are not touched by clearing - assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId)) - } - - @Test - fun testGetMinRepositoryWeight() { - assertEquals(Int.MAX_VALUE, repoDao.getMinRepositoryWeight()) - - repoDao.insertOrReplace(getRandomRepo()) - assertEquals(Int.MAX_VALUE - 2, repoDao.getMinRepositoryWeight()) - - repoDao.insertOrReplace(getRandomRepo()) - assertEquals(Int.MAX_VALUE - 4, repoDao.getMinRepositoryWeight()) - } - - @Test - fun testReorderRepositories() { - val repoId1 = repoDao.insertOrReplace(getRandomRepo()) - val repoId2 = repoDao.insertOrReplace(getRandomRepo()) - val repoId3 = repoDao.insertOrReplace(getRandomRepo()) - val repoId4 = repoDao.insertOrReplace(getRandomRepo()) - val repoId5 = repoDao.insertOrReplace(getRandomRepo()) - - // repos are listed in the order they entered the DB [1, 2, 3, 4, 5] - assertEquals( - listOf(repoId1, repoId2, repoId3, repoId4, repoId5), - repoDao.getRepositories().map { it.repoId }, - ) - - // 2 gets moved to 5 [1, 3, 4, 5, 2] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId2) ?: fail(), - repoTarget = repoDao.getRepository(repoId5) ?: fail(), - ) - assertEquals( - listOf(repoId1, repoId3, repoId4, repoId5, repoId2), - repoDao.getRepositories().map { it.repoId }, - ) - - // 5 gets moved to 1 [5, 1, 3, 4, 2] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId5) ?: fail(), - repoTarget = repoDao.getRepository(repoId1) ?: fail(), - ) - assertEquals( - listOf(repoId5, repoId1, repoId3, repoId4, repoId2), - repoDao.getRepositories().map { it.repoId }, - ) - - // 3 gets moved to 5 [3, 5, 1, 4, 2] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId3) ?: fail(), - repoTarget = repoDao.getRepository(repoId5) ?: fail(), - ) - assertEquals( - listOf(repoId3, repoId5, repoId1, repoId4, repoId2), - repoDao.getRepositories().map { it.repoId }, - ) - - // 3 gets moved to itself, list shouldn't change [3, 5, 1, 4, 2] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId3) ?: fail(), - repoTarget = repoDao.getRepository(repoId3) ?: fail(), - ) - assertEquals( - listOf(repoId3, repoId5, repoId1, repoId4, repoId2), - repoDao.getRepositories().map { it.repoId }, - ) - - // we'll add an archive repo for repo1 to the list [3, 5, (1, 1a), 4, 2] - val repo1 = repoDao.getRepository(repoId1) ?: fail() - repoDao.updateRepository(repo1.repository.copy(certificate = "1234abcd")) - val repo1a = InitialRepository( - name = getRandomString(), - address = "https://example.org/archive", - description = getRandomString(), - certificate = "1234abcd", // same as repo1 - version = 42L, - enabled = false, - ) - val repoId1a = repoDao.insert(repo1a) - repoDao.setWeight(repoId1a, repo1.weight - 1) - - // now we move repo 1 to position of repo 2 [3, 5, 4, 2, (1, 1a)] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId1) ?: fail(), - repoTarget = repoDao.getRepository(repoId2) ?: fail(), - ) - assertEquals( - listOf(repoId3, repoId5, repoId4, repoId2, repoId1, repoId1a), - repoDao.getRepositories().map { it.repoId }, - ) - - // now move repo 1 and its archive to top position [(1, 1a), 3, 5, 4, 2] - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId1) ?: fail(), - repoTarget = repoDao.getRepository(repoId3) ?: fail(), - ) - assertEquals( - listOf(repoId1, repoId1a, repoId3, repoId5, repoId4, repoId2), - repoDao.getRepositories().map { it.repoId }, - ) - - // archive repos can't be reordered directly - assertFailsWith { - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId1a) ?: fail(), - repoTarget = repoDao.getRepository(repoId3) ?: fail(), - ) - } - assertFailsWith { - repoDao.reorderRepositories( - repoToReorder = repoDao.getRepository(repoId3) ?: fail(), - repoTarget = repoDao.getRepository(repoId1a) ?: fail(), - ) - } + assertFailsWith { + repoDao.reorderRepositories( + repoToReorder = repoDao.getRepository(repoId3) ?: fail(), + repoTarget = repoDao.getRepository(repoId1a) ?: fail(), + ) } + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDiffTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDiffTest.kt index 432d70a8e..d7d501f2a 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDiffTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDiffTest.kt @@ -1,6 +1,9 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertTrue import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import org.fdroid.database.TestUtils.assertRepoEquals @@ -20,191 +23,210 @@ 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 -import kotlin.test.assertTrue -/** - * Tests that repository diffs get applied to the database correctly. - */ +/** Tests that repository diffs get applied to the database correctly. */ @RunWith(AndroidJUnit4::class) internal class RepositoryDiffTest : DbTest() { - private val j = Json + private val j = Json - @Test - fun timestampDiff() { - val repo = getRandomRepo() - val updateTimestamp = repo.timestamp + 1 - val 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]) - } + }""" + .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) + @Test + fun timestampDiffTwoReposInDb() { + // insert repo + val repo = getRandomRepo() + repoDao.insertOrReplace(repo) - // insert another repo before updating - repoDao.insertOrReplace(getRandomRepo()) + // 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 + // 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 = """ + val updateTimestamp = Random.nextLong() + val json = + """ { "timestamp": $updateTimestamp, "unknown": "field" - }""".trimIndent() + }""" + .trimIndent() - // decode diff from JSON and update DB with it - val diff = j.parseToJsonElement(json).jsonObject // Json.decodeFromString(json) - repoDao.updateRepository(repoId, 42, diff) + // decode diff from JSON and update DB with it + val diff = j.parseToJsonElement(json).jsonObject // Json.decodeFromString(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]) - } + // 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 = """ + @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]) - } + }""" + .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 mirrorUnknownKeyDiff() { - val repo = getRandomRepo() - val json = """ - { - "mirrors": [ - { "url": "foo", "countryCode": "bar", "unknown": "doesntexist" } - ] - }""".trimIndent() - testDiff(repo, json) { repos -> - val expectedMirrors = setOf(Mirror(repos[0].repoId, "foo", "bar")) - assertEquals(expectedMirrors, repos[0].mirrors.toSet()) - assertRepoEquals(repo.copy(mirrors = listOf(MirrorV2("foo", "bar"))), repos[0]) - } + @Test + fun mirrorUnknownKeyDiff() { + val repo = getRandomRepo() + val json = + """ + { + "mirrors": [ + { "url": "foo", "countryCode": "bar", "unknown": "doesntexist" } + ] + } + """ + .trimIndent() + testDiff(repo, json) { repos -> + val expectedMirrors = setOf(Mirror(repos[0].repoId, "foo", "bar")) + assertEquals(expectedMirrors, repos[0].mirrors.toSet()) + assertRepoEquals(repo.copy(mirrors = listOf(MirrorV2("foo", "bar"))), 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 = """ + @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]) - } + }""" + .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 = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - }) - val antiFeatures = repo.antiFeatures.randomDiff { - AntiFeatureV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - } - val json = """ + @Test + fun antiFeaturesDiff() { + val repo = + getRandomRepo() + .copy( + antiFeatures = + getRandomMap { + getRandomString() to + AntiFeatureV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + } + ) + val antiFeatures = + repo.antiFeatures.randomDiff { + AntiFeatureV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = 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]) - } + }""" + .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 = getRandomLocalizedFileV2(), + @Test + fun antiFeatureKeyChangeDiff() { + val antiFeatureKey = getRandomString() + val antiFeature = + AntiFeatureV2( + icon = getRandomLocalizedFileV2(), + 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 = emptyMap(), 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 = emptyMap(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - ) - val json = """ + ) + ) + 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]) - } + }""" + .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 antiFeaturesRemovedOptionalsDiff() { - val antiFeatureKey = getRandomString() - val antiFeature = AntiFeatureV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - val antiFeatures = mapOf(antiFeatureKey to antiFeature) - val repo = getRandomRepo().copy(antiFeatures = antiFeatures) - val json = """ + @Test + fun antiFeaturesRemovedOptionalsDiff() { + val antiFeatureKey = getRandomString() + val antiFeature = + AntiFeatureV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + val antiFeatures = mapOf(antiFeatureKey to antiFeature) + val repo = getRandomRepo().copy(antiFeatures = antiFeatures) + val json = + """ { "antiFeatures": { "$antiFeatureKey": { @@ -212,54 +234,70 @@ internal class RepositoryDiffTest : DbTest() { "description": null } } - }""".trimIndent() - testDiff(repo, json) { repos -> - // icon and description maps were emptied (not nulled as they aren't nullable) - assertEquals(emptyMap(), repos[0].getAntiFeatures()[antiFeatureKey]!!.icon) - assertEquals(emptyMap(), repos[0].getAntiFeatures()[antiFeatureKey]!!.description) - } + }""" + .trimIndent() + testDiff(repo, json) { repos -> + // icon and description maps were emptied (not nulled as they aren't nullable) + assertEquals(emptyMap(), repos[0].getAntiFeatures()[antiFeatureKey]!!.icon) + assertEquals(emptyMap(), repos[0].getAntiFeatures()[antiFeatureKey]!!.description) } + } - @Test - fun categoriesDiff() { - val repo = getRandomRepo().copy(categories = getRandomMap { - getRandomString() to CategoryV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - }) - val categories = repo.categories.randomDiff { - CategoryV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - } - val json = """ + @Test + fun categoriesDiff() { + val repo = + getRandomRepo() + .copy( + categories = + getRandomMap { + getRandomString() to + CategoryV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + } + ) + val categories = + repo.categories.randomDiff { + CategoryV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + } + val json = + """ { "categories": ${Json.encodeToString(categories)} - }""".trimIndent() - testDiff(repo, json) { repos -> - val expectedCategories = repo.categories.applyDiff(categories) - val expectedRepoCategories = - expectedCategories.toRepoCategories(repos[0].repoId) - assertEquals(expectedRepoCategories.toSet(), repos[0].categories.toSet()) - assertRepoEquals(repo.copy(categories = expectedCategories), repos[0]) - } + }""" + .trimIndent() + testDiff(repo, json) { repos -> + val expectedCategories = repo.categories.applyDiff(categories) + val expectedRepoCategories = expectedCategories.toRepoCategories(repos[0].repoId) + assertEquals(expectedRepoCategories.toSet(), repos[0].categories.toSet()) + assertRepoEquals(repo.copy(categories = expectedCategories), repos[0]) } + } - @Test - fun categoriesRemovedOptionalsDiff() { - val categoryId = getRandomString() - val repo = getRandomRepo().copy(categories = getRandomMap { - categoryId to CategoryV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - }) - val json = """ + @Test + fun categoriesRemovedOptionalsDiff() { + val categoryId = getRandomString() + val repo = + getRandomRepo() + .copy( + categories = + getRandomMap { + categoryId to + CategoryV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + } + ) + val json = + """ { "categories": { "$categoryId": { @@ -267,48 +305,64 @@ internal class RepositoryDiffTest : DbTest() { "description": null } } - }""".trimIndent() - testDiff(repo, json) { repos -> - // icon and description maps were emptied (not nulled as they aren't nullable) - assertEquals(emptyMap(), repos[0].getCategories()[categoryId]!!.icon) - assertEquals(emptyMap(), repos[0].getCategories()[categoryId]!!.description) - } + }""" + .trimIndent() + testDiff(repo, json) { repos -> + // icon and description maps were emptied (not nulled as they aren't nullable) + assertEquals(emptyMap(), repos[0].getCategories()[categoryId]!!.icon) + assertEquals(emptyMap(), repos[0].getCategories()[categoryId]!!.description) } + } - @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 = """ + @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]) - } + }""" + .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]) } + } - @Test - fun releaseChannelsRemovedOptionalsDiff() { - val releaseChannelKey = getRandomString() - val repo = getRandomRepo().copy(releaseChannels = getRandomMap { - releaseChannelKey to ReleaseChannelV2( - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), - ) - }) - val json = """ + @Test + fun releaseChannelsRemovedOptionalsDiff() { + val releaseChannelKey = getRandomString() + val repo = + getRandomRepo() + .copy( + releaseChannels = + getRandomMap { + releaseChannelKey to + ReleaseChannelV2( + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), + ) + } + ) + val json = + """ { "releaseChannels": { "$releaseChannelKey": { @@ -316,88 +370,99 @@ internal class RepositoryDiffTest : DbTest() { "icon": null } } - }""".trimIndent() - testDiff(repo, json) { repos -> - // icon and description maps were emptied (not nulled as they aren't nullable) - assertEquals(emptyMap(), repos[0].getReleaseChannels()[releaseChannelKey]!!.icon) - assertEquals(emptyMap(), repos[0].getReleaseChannels()[releaseChannelKey]!!.description) - } + }""" + .trimIndent() + testDiff(repo, json) { repos -> + // icon and description maps were emptied (not nulled as they aren't nullable) + assertEquals(emptyMap(), repos[0].getReleaseChannels()[releaseChannelKey]!!.icon) + assertEquals(emptyMap(), repos[0].getReleaseChannels()[releaseChannelKey]!!.description) } + } - @Test - fun removeAllOptionalsDiff() { - val repo = getRandomRepo().copy( - // ensure optional values exist - name = getRandomLocalizedTextV2(2), - webBaseUrl = getRandomString(), - mirrors = getRandomList(2) { getRandomMirror() }, - antiFeatures = getRandomMap(2) { - getRandomString() to AntiFeatureV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), + @Test + fun removeAllOptionalsDiff() { + val repo = + getRandomRepo() + .copy( + // ensure optional values exist + name = getRandomLocalizedTextV2(2), + webBaseUrl = getRandomString(), + mirrors = getRandomList(2) { getRandomMirror() }, + antiFeatures = + getRandomMap(2) { + getRandomString() to + AntiFeatureV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), ) }, - categories = getRandomMap(2) { - getRandomString() to CategoryV2( - icon = getRandomLocalizedFileV2(), - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), + categories = + getRandomMap(2) { + getRandomString() to + CategoryV2( + icon = getRandomLocalizedFileV2(), + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), ) }, - releaseChannels = getRandomMap(2) { - getRandomString() to ReleaseChannelV2( - name = getRandomLocalizedTextV2(), - description = getRandomLocalizedTextV2(), + releaseChannels = + getRandomMap(2) { + getRandomString() to + ReleaseChannelV2( + name = getRandomLocalizedTextV2(), + description = getRandomLocalizedTextV2(), ) }, ) - assertTrue(repo.name.isNotEmpty()) - assertTrue(repo.icon.isNotEmpty()) - assertTrue(!repo.webBaseUrl.isNullOrEmpty()) - assertTrue(repo.mirrors.isNotEmpty()) - assertTrue(repo.antiFeatures.isNotEmpty()) - assertTrue(repo.categories.isNotEmpty()) - assertTrue(repo.releaseChannels.isNotEmpty()) - val json = """ - { - "name": null, - "icon": null, - "webBaseUrl": null, - "mirrors": null, - "antiFeatures": null, - "categories": null, - "releaseChannels": null - }""".trimIndent() - testDiff(repo, json) { repos -> - assertEquals(emptyMap(), repos[0].repository.name) - assertEquals(null, repos[0].repository.icon) - assertEquals(null, repos[0].webBaseUrl) - assertEquals(emptyList(), repos[0].mirrors) - assertEquals(emptyMap(), repos[0].getAntiFeatures()) - assertEquals(emptyMap(), repos[0].getCategories()) - assertEquals(emptyMap(), repos[0].getReleaseChannels()) - } + assertTrue(repo.name.isNotEmpty()) + assertTrue(repo.icon.isNotEmpty()) + assertTrue(!repo.webBaseUrl.isNullOrEmpty()) + assertTrue(repo.mirrors.isNotEmpty()) + assertTrue(repo.antiFeatures.isNotEmpty()) + assertTrue(repo.categories.isNotEmpty()) + assertTrue(repo.releaseChannels.isNotEmpty()) + val json = + """ + { + "name": null, + "icon": null, + "webBaseUrl": null, + "mirrors": null, + "antiFeatures": null, + "categories": null, + "releaseChannels": null + } + """ + .trimIndent() + testDiff(repo, json) { repos -> + assertEquals(emptyMap(), repos[0].repository.name) + assertEquals(null, repos[0].repository.icon) + assertEquals(null, repos[0].webBaseUrl) + assertEquals(emptyList(), repos[0].mirrors) + assertEquals(emptyMap(), repos[0].getAntiFeatures()) + assertEquals(emptyMap(), repos[0].getCategories()) + assertEquals(emptyMap(), repos[0].getReleaseChannels()) } + } - private fun testDiff(repo: RepoV2, json: String, repoChecker: (List) -> Unit) { - // insert repo - repoDao.insertOrReplace(repo) + private fun testDiff(repo: RepoV2, json: String, repoChecker: (List) -> 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 + // 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) - } + // 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) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/TestUtils.kt b/libs/database/src/dbTest/java/org/fdroid/database/TestUtils.kt index 745906c67..0933a51b7 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/TestUtils.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/TestUtils.kt @@ -2,119 +2,126 @@ 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 +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 internal object TestUtils { - fun assertTimestampRecent(timestamp: Long?) { - assertNotNull(timestamp) - assertTrue(System.currentTimeMillis() - timestamp < 2000) - } + 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.toMirrors(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).sortedBy { it.id } - assertEquals(expectedCategories, repo.categories.sortedBy { it.id }) - // 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) - } + fun assertRepoEquals(repoV2: RepoV2, repo: Repository) { + val repoId = repo.repoId + // mirrors + val expectedMirrors = repoV2.mirrors.toMirrors(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).sortedBy { it.id } + assertEquals(expectedCategories, repo.categories.sortedBy { it.id }) + // 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, + 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(), + 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(), + releaseChannels = releaseChannels, + antiFeatures = version.antiFeatures ?: emptyMap(), + whatsNew = version.whatsNew ?: emptyMap(), ) - fun LiveData.getOrAwaitValue(): T? { - val data = arrayOfNulls(1) - val latch = CountDownLatch(1) - val observer: Observer = object : Observer { - override fun onChanged(value: T) { - data[0] = value - latch.countDown() - removeObserver(this) - } + fun LiveData.getOrAwaitValue(): T? { + val data = arrayOfNulls(1) + val latch = CountDownLatch(1) + val observer: Observer = + object : Observer { + override fun onChanged(value: T) { + data[0] = value + latch.countDown() + removeObserver(this) } - observeForever(observer) - latch.await(2, TimeUnit.SECONDS) - @Suppress("UNCHECKED_CAST") - return data[0] as T? - } + } + observeForever(observer) + latch.await(2, TimeUnit.SECONDS) + @Suppress("UNCHECKED_CAST") + return data[0] as T? + } - fun LiveData.getOrFail(): T { - return getOrAwaitValue() ?: fail() - } + fun LiveData.getOrFail(): T { + return getOrAwaitValue() ?: fail() + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt index bbf2b3174..4b1ddf910 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt @@ -1,6 +1,9 @@ package org.fdroid.database import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.fail import org.fdroid.database.TestUtils.getOrFail import org.fdroid.index.v2.PackageVersionV2 import org.fdroid.test.TestAppUtils.getRandomMetadataV2 @@ -9,255 +12,253 @@ 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.fail @RunWith(AndroidJUnit4::class) internal class VersionTest : DbTest() { - 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 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 getAppVersion1(repoId: Long): AppVersion { + val version = getVersion1(repoId) + return AppVersion( + version = version, + versionedStrings = packageVersion1.manifest.getVersionedStrings(version), ) + } - private fun getAppVersion1(repoId: Long): AppVersion { - val version = getVersion1(repoId) - return AppVersion( - version = version, - versionedStrings = packageVersion1.manifest.getVersionedStrings(version), - ) + private fun getAppVersion2(repoId: Long): AppVersion { + val version = getVersion2(repoId) + return AppVersion( + version = version, + versionedStrings = packageVersion2.manifest.getVersionedStrings(version), + ) + } + + 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() } + } - private fun getAppVersion2(repoId: Long): AppVersion { - val version = getVersion2(repoId) - return AppVersion( - version = version, - versionedStrings = packageVersion2.manifest.getVersionedStrings(version), - ) - } + @Test + fun insertGetDeleteSingleVersion() { + val repoId = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId, packageName, getRandomMetadataV2()) + versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1) - private fun getVersion1(repoId: Long) = - packageVersion1.toVersion(repoId, packageName, versionId1, isCompatible1) + val appVersions = versionDao.getAppVersions(repoId, packageName).getOrFail() + assertEquals(1, appVersions.size) + assertEquals(getAppVersion1(repoId), appVersions[0]) - private fun getVersion2(repoId: Long) = - packageVersion2.toVersion(repoId, packageName, versionId2, isCompatible2) + val manifest = packageVersion1.manifest + val versionedStrings = versionDao.getVersionedStrings(repoId, packageName) + val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size + assertEquals(expectedSize, versionedStrings.size) - private val compatChecker: (PackageVersionV2) -> Boolean = { - when (it.file.sha256) { - versionId1 -> isCompatible1 - versionId2 -> isCompatible2 - else -> fail() - } - } + // getting version by repo produces same result + val versionsByRepo = versionDao.getAppVersions(repoId, packageName).getOrFail() + assertEquals(1, versionsByRepo.size) + assertEquals(getAppVersion1(repoId), versionsByRepo[0]) - @Test - fun insertGetDeleteSingleVersion() { - val repoId = repoDao.insertOrReplace(getRandomRepo()) - appDao.insert(repoId, packageName, getRandomMetadataV2()) - versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1) + versionDao.deleteAppVersion(repoId, packageName, versionId1) + assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) + assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) + } - val appVersions = versionDao.getAppVersions(repoId, packageName).getOrFail() - assertEquals(1, appVersions.size) - assertEquals(getAppVersion1(repoId), appVersions[0]) + @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) - val manifest = packageVersion1.manifest - val versionedStrings = versionDao.getVersionedStrings(repoId, packageName) - val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size - assertEquals(expectedSize, versionedStrings.size) + // get app versions from DB and assign them correctly + listOf( + versionDao.getAppVersions(packageName).getOrFail(), + versionDao.getAppVersions(repoId, packageName).getOrFail(), + ) + .forEach { appVersions -> + 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] - // getting version by repo produces same result - val versionsByRepo = versionDao.getAppVersions(repoId, packageName).getOrFail() - assertEquals(1, versionsByRepo.size) - assertEquals(getAppVersion1(repoId), versionsByRepo[0]) + // check first version matches + assertEquals(getAppVersion1(repoId), appVersion) - versionDao.deleteAppVersion(repoId, packageName, versionId1) - assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) - assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) - } + // check second version matches + assertEquals(getAppVersion2(repoId), appVersion2) + } - @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) + // delete app and check that all associated data also gets deleted + appDao.deleteAppMetadata(repoId, packageName) + assertEquals(0, versionDao.getAppVersions(packageName).getOrFail().size) + assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) + assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) + } - // get app versions from DB and assign them correctly - listOf( - versionDao.getAppVersions(packageName).getOrFail(), - versionDao.getAppVersions(repoId, packageName).getOrFail(), - ).forEach { appVersions -> - 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] + @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) - // check first version matches - assertEquals(getAppVersion1(repoId), appVersion) + // 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) - // check second version matches - assertEquals(getAppVersion2(repoId), appVersion2) - } + // query by repo only returns the versions from each repo + assertEquals(2, versionDao.getAppVersions(repoId, packageName).getOrFail().size) + assertEquals(1, versionDao.getAppVersions(repoId2, packageName).getOrFail().size) - // delete app and check that all associated data also gets deleted - appDao.deleteAppMetadata(repoId, packageName) - assertEquals(0, versionDao.getAppVersions(packageName).getOrFail().size) - assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) - assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) - } + // disable second repo + repoDao.setRepositoryEnabled(repoId2, false) - @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) + // now only two versions get returned + 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) + @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)) + val versions3 = versionDao.getAppVersions(repoId, packageName).getOrFail() + assertEquals(3, versions1.size) + assertEquals(3, versions2.size) + assertEquals(3, versions3.size) - // query by repo only returns the versions from each repo - assertEquals(2, versionDao.getAppVersions(repoId, packageName).getOrFail().size) - assertEquals(1, versionDao.getAppVersions(repoId2, packageName).getOrFail().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) + assertEquals(versionCode, versions3[i].version.manifest.versionCode) + } + } - // disable second repo - repoDao.setRepositoryEnabled(repoId2, false) + @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) - // now only two versions get returned - assertEquals(2, versionDao.getAppVersions(packageName).getOrFail().size) - assertEquals(2, 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) - @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)) - val versions3 = versionDao.getAppVersions(repoId, packageName).getOrFail() - assertEquals(3, versions1.size) - assertEquals(3, versions2.size) - assertEquals(3, versions3.size) + // ignore lower version code doesn't change result + appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode - 1) + appPrefsDao.update(appPrefs) + assertEquals(1, versionDao.getVersions(listOf(packageName)).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) - assertEquals(versionCode, versions3[i].version.manifest.versionCode) - } - } + // ignoring exact version code does change result + appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode) + appPrefsDao.update(appPrefs) + assertEquals(0, versionDao.getVersions(listOf(packageName)).size) - @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) + // ignoring higher version code does change result + appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode + 1) + appPrefsDao.update(appPrefs) + assertEquals(0, 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) + // ignoring all updates does change result + appPrefs = appPrefs.toggleIgnoreAllUpdates() + appPrefsDao.update(appPrefs) + assertEquals(0, 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) + // not ignoring all updates brings back version + appPrefs = appPrefs.toggleIgnoreAllUpdates() + 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) + // clear all apps and their versions + appDao.clearAll() + assertEquals(0, versionDao.countAppVersions()) + assertEquals(0, versionDao.countVersionedStrings()) + } - // ignoring higher version code does change result - appPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(versionCode + 1) - appPrefsDao.update(appPrefs) - assertEquals(0, 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) - // ignoring all updates does change result - appPrefs = appPrefs.toggleIgnoreAllUpdates() - appPrefsDao.update(appPrefs) - assertEquals(0, 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) - // not ignoring all updates brings back version - appPrefs = appPrefs.toggleIgnoreAllUpdates() - appPrefsDao.update(appPrefs) - assertEquals(1, versionDao.getVersions(listOf(packageName)).size) + // still only returns above versions + assertEquals(2, versionDao.getVersions(listOf(packageName)).size) - // clear all apps and their versions - appDao.clearAll() - assertEquals(0, versionDao.countAppVersions()) - assertEquals(0, versionDao.countVersionedStrings()) - } + // all versions are returned only if all packages are asked for + assertEquals(4, versionDao.getVersions(listOf(packageName, packageName2)).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) + @Test + fun getVersionsHandlesMaxVariableNumber() { + // sqlite has a maximum number of 999 variables that can be used in a query + val packagesOk = MutableList(998) { "" } + listOf(packageName) + val packagesNotOk1 = MutableList(1000) { "" } + listOf(packageName) + val packagesNotOk2 = MutableList(5000) { "" } + listOf(packageName) - // 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) - } - - @Test - fun getVersionsHandlesMaxVariableNumber() { - // sqlite has a maximum number of 999 variables that can be used in a query - val packagesOk = MutableList(998) { "" } + listOf(packageName) - val packagesNotOk1 = MutableList(1000) { "" } + listOf(packageName) - val packagesNotOk2 = MutableList(5000) { "" } + listOf(packageName) - - // 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) - - // versions are returned as expected for all lists, no matter their size - assertEquals(2, versionDao.getVersions(packagesOk).size) - assertEquals(2, versionDao.getVersions(packagesNotOk1).size) - assertEquals(2, versionDao.getVersions(packagesNotOk2).size) - } + // 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) + // versions are returned as expected for all lists, no matter their size + assertEquals(2, versionDao.getVersions(packagesOk).size) + assertEquals(2, versionDao.getVersions(packagesNotOk1).size) + assertEquals(2, versionDao.getVersions(packagesNotOk2).size) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/index/IndexUpdaterTest.kt b/libs/database/src/dbTest/java/org/fdroid/index/IndexUpdaterTest.kt index aff1da593..40b72de31 100644 --- a/libs/database/src/dbTest/java/org/fdroid/index/IndexUpdaterTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/index/IndexUpdaterTest.kt @@ -1,29 +1,29 @@ package org.fdroid.index import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlin.test.assertEquals import org.fdroid.database.Repository import org.fdroid.index.v2.SIGNED_FILE_NAME import org.junit.Test import org.junit.runner.RunWith -import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) internal class IndexUpdaterTest { - @Test - fun testDefaultUriBuilder() { - val repo = Repository( - repoId = 42L, - address = "http://example.org/", - timestamp = 1337L, - formatVersion = IndexFormatVersion.TWO, - certificate = "abcd", - version = 2001, - weight = 0, - lastUpdated = 23L, - ) - val uri = defaultRepoUriBuilder.getUri(repo, SIGNED_FILE_NAME) - assertEquals("http://example.org/$SIGNED_FILE_NAME", uri.toString()) - } - + @Test + fun testDefaultUriBuilder() { + val repo = + Repository( + repoId = 42L, + address = "http://example.org/", + timestamp = 1337L, + formatVersion = IndexFormatVersion.TWO, + certificate = "abcd", + version = 2001, + weight = 0, + lastUpdated = 23L, + ) + val uri = defaultRepoUriBuilder.getUri(repo, SIGNED_FILE_NAME) + assertEquals("http://example.org/$SIGNED_FILE_NAME", uri.toString()) + } } diff --git a/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt b/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt index b8706492d..bd6bc9be7 100644 --- a/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/index/v1/IndexV1UpdaterTest.kt @@ -7,6 +7,13 @@ import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk +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 import org.fdroid.CompatibilityChecker import org.fdroid.database.DbTest import org.fdroid.database.Repository @@ -19,220 +26,209 @@ import org.fdroid.index.SigningException import org.fdroid.index.TempFileProvider import org.fdroid.index.v2.ANTI_FEATURE_KNOWN_VULNERABILITY import org.fdroid.index.v2.FileV2 +import org.fdroid.test.TestUtils.getRes 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 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: IndexV1Updater + 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, - ) + @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, certificate = TESTY_CERT) + val repo = repoDao.getRepository(repoId) ?: fail() + downloadIndex(repo, TESTY_JAR) + val result = indexUpdater.update(repo).noError() + assertIs(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()) } - - @Test - fun testIndexV1Processing() { - val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL, certificate = TESTY_CERT) - val repo = repoDao.getRepository(repoId) ?: fail() - downloadIndex(repo, TESTY_JAR) - val result = indexUpdater.update(repo).noError() - assertIs(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(result2) + 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(), + ) - @Test - fun testIndexV1WithWrongCert() { - val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL) - val repo = repoDao.getRepository(repoId) ?: fail() - downloadIndex(repo, TESTY_JAR) - val result = indexUpdater.update(repo) - assertIs(result) - assertIs(result.e) + // 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) - // check that the DB transaction was rolled back and the DB wasn't changed - // except for adding the error to the repo - val expectedRepo = repo.copy( - preferences = repo.preferences.copy( - errorCount = 1, - lastError = "Signing certificate does not match" - ), - ) - assertEquals(expectedRepo, repoDao.getRepository(repoId) ?: fail()) - assertEquals(0, appDao.countApps()) - assertEquals(0, versionDao.countAppVersions()) - } + 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) - @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.update(futureRepo) - assertIs(result) - assertIs(result.e) - assertFalse((result.e as OldIndexException).isSameTimestamp) - } + // update again and get unchanged + downloadIndex(updatedRepo, TESTY_JAR) + val result2 = indexUpdater.update(updatedRepo).noError() + assertIs(result2) + } - @Test - fun testIndexV1WithCorruptAppPackageName() { - val result = testBadTestyJar("testy.at.or.at_corrupt_app_package_name_index-v1.jar") - assertIs(result) - } + @Test + fun testIndexV1WithWrongCert() { + val repoId = repoDao.insertEmptyRepo(TESTY_CANONICAL_URL) + val repo = repoDao.getRepository(repoId) ?: fail() + downloadIndex(repo, TESTY_JAR) + val result = indexUpdater.update(repo) + assertIs(result) + assertIs(result.e) - @Test - fun testIndexV1WithCorruptPackageName() { - val result = testBadTestyJar("testy.at.or.at_corrupt_package_name_index-v1.jar") - assertIs(result) - } + // check that the DB transaction was rolled back and the DB wasn't changed + // except for adding the error to the repo + val expectedRepo = + repo.copy( + preferences = + repo.preferences.copy(errorCount = 1, lastError = "Signing certificate does not match") + ) + assertEquals(expectedRepo, repoDao.getRepository(repoId) ?: fail()) + assertEquals(0, appDao.countApps()) + assertEquals(0, versionDao.countAppVersions()) + } - @Test - fun testIndexV1WithBadTestyJarNoManifest() { - val result = testBadTestyJar("testy.at.or.at_no-MANIFEST.MF_index-v1.jar") - assertIs(result) - assertIs(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.update(futureRepo) + assertIs(result) + assertIs(result.e) + assertFalse((result.e as OldIndexException).isSameTimestamp) + } - @Test - fun testIndexV1WithBadTestyJarNoSigningCert() { - val result = testBadTestyJar("testy.at.or.at_no-.RSA_index-v1.jar") - assertIs(result) - } + @Test + fun testIndexV1WithCorruptAppPackageName() { + val result = testBadTestyJar("testy.at.or.at_corrupt_app_package_name_index-v1.jar") + assertIs(result) + } - @Test - fun testIndexV1WithBadTestyJarNoSignature() { - val result = testBadTestyJar("testy.at.or.at_no-.SF_index-v1.jar") - assertIs(result) - } + @Test + fun testIndexV1WithCorruptPackageName() { + val result = testBadTestyJar("testy.at.or.at_corrupt_package_name_index-v1.jar") + assertIs(result) + } - @Test - fun testIndexV1WithBadTestyJarNoSignatureFiles() { - val result = testBadTestyJar("testy.at.or.at_no-signature_index-v1.jar") - assertIs(result) - assertIs(result.e) - } + @Test + fun testIndexV1WithBadTestyJarNoManifest() { + val result = testBadTestyJar("testy.at.or.at_no-MANIFEST.MF_index-v1.jar") + assertIs(result) + assertIs(result.e) + } - @Suppress("DEPRECATION") - private fun downloadIndex(repo: Repository, jar: String) { - val uri = Uri.parse("${repo.address}/$SIGNED_FILE_NAME") - val indexFile = FileV2.fromPath("/$SIGNED_FILE_NAME") + @Test + fun testIndexV1WithBadTestyJarNoSigningCert() { + val result = testBadTestyJar("testy.at.or.at_no-.RSA_index-v1.jar") + assertIs(result) + } - val jarFile = tmpFolder.newFile() - assets.open(jar).use { inputStream -> - jarFile.outputStream().use { inputStream.copyTo(it) } - } - every { tempFileProvider.createTempFile(null) } returns jarFile - every { - downloaderFactory.createWithTryFirstMirror(repo, uri, indexFile, jarFile) - } returns downloader - every { downloader.cacheTag = null } just Runs - every { downloader.download() } just Runs - every { downloader.hasChanged() } returns true - every { downloader.cacheTag } returns null - } + @Test + fun testIndexV1WithBadTestyJarNoSignature() { + val result = testBadTestyJar("testy.at.or.at_no-.SF_index-v1.jar") + assertIs(result) + } - private fun testBadTestyJar(jar: String): IndexUpdateResult { - val repoId = repoDao.insertEmptyRepo("http://example.org") - val repo = repoDao.getRepository(repoId) ?: fail() - downloadIndex(repo, jar) - return indexUpdater.update(repo) - } + @Test + fun testIndexV1WithBadTestyJarNoSignatureFiles() { + val result = testBadTestyJar("testy.at.or.at_no-signature_index-v1.jar") + assertIs(result) + assertIs(result.e) + } - /** - * Easier for debugging, if we throw the index error. - */ - private fun IndexUpdateResult.noError(): IndexUpdateResult { - if (this is IndexUpdateResult.Error) throw e - return this - } + @Suppress("DEPRECATION") + private fun downloadIndex(repo: Repository, jar: String) { + val uri = Uri.parse("${repo.address}/$SIGNED_FILE_NAME") + val indexFile = FileV2.fromPath("/$SIGNED_FILE_NAME") + val jarFile = tmpFolder.newFile() + getRes(jar).use { inputStream -> jarFile.outputStream().use { inputStream.copyTo(it) } } + every { tempFileProvider.createTempFile(null) } returns jarFile + every { downloaderFactory.createWithTryFirstMirror(repo, uri, indexFile, 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.update(repo) + } + + /** 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" + + "818e469465f96b704e27be2fee4c63ab9f83ddf30e7a34c7371a4728d83b0bc1" +private const val TESTY_CERT = + "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b" + "050030213110300e060355040b1307462d44726f6964310d300b060355040313" + "04736f7661301e170d3136303832333133333131365a170d3434303130393133" + "333131365a30213110300e060355040b1307462d44726f6964310d300b060355" + diff --git a/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt b/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt index ed8bcfea2..f20b518fd 100644 --- a/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/index/v2/IndexV2UpdaterTest.kt @@ -8,6 +8,11 @@ import io.mockk.just import io.mockk.mockk import io.mockk.slot import io.mockk.spyk +import java.util.concurrent.CountDownLatch +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.fail import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async @@ -26,313 +31,318 @@ import org.fdroid.test.TestDataEntry import org.fdroid.test.TestDataMaxV2 import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 +import org.fdroid.test.TestUtils.getRes import org.fdroid.test.VerifierConstants.CERTIFICATE import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNull -import kotlin.test.fail @RunWith(AndroidJUnit4::class) internal class IndexV2UpdaterTest : DbTest() { - @get:Rule - var tmpFolder: TemporaryFolder = TemporaryFolder() + @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 + 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, - ) - } + @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", certificate = CERTIFICATE) - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "index-min-v2.json", - indexFileV2 = TestDataEntry.emptyToMin.index - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Processed, result) - assertDbEquals(repoId, TestDataMinV2.index) + @Test + fun testFullIndexEmptyToMin() { + val repoId = repoDao.insertEmptyRepo("http://example.org", certificate = CERTIFICATE) + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "index-min-v2.json", + indexFileV2 = TestDataEntry.emptyToMin.index, + ) + val result = indexUpdater.update(repo).noError() + assertEquals(IndexUpdateResult.Processed, result) + assertDbEquals(repoId, TestDataMinV2.index) - // check that format version got entered and certificate stayed the same - val updatedRepo = repoDao.getRepository(repoId) ?: fail() - assertEquals(TWO, updatedRepo.formatVersion) - assertEquals(CERTIFICATE, updatedRepo.certificate) - assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated) - } + // check that format version got entered and certificate stayed the same + 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", certificate = CERTIFICATE) - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-mid/$SIGNED_FILE_NAME", - jsonPath = "index-mid-v2.json", - indexFileV2 = TestDataEntry.emptyToMid.index - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Processed, result) - assertDbEquals(repoId, TestDataMidV2.index) - assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated) - } + @Test + fun testFullIndexEmptyToMid() { + val repoId = repoDao.insertEmptyRepo("http://example.org", certificate = CERTIFICATE) + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-mid/$SIGNED_FILE_NAME", + jsonPath = "index-mid-v2.json", + indexFileV2 = TestDataEntry.emptyToMid.index, + ) + val result = indexUpdater.update(repo).noError() + assertEquals(IndexUpdateResult.Processed, result) + assertDbEquals(repoId, TestDataMidV2.index) + assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated) + } - @Test - fun testFullIndexEmptyToMax() { - val repoId = repoDao.insertEmptyRepo("http://example.org", certificate = CERTIFICATE) - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-max/$SIGNED_FILE_NAME", - jsonPath = "index-max-v2.json", - indexFileV2 = TestDataEntry.emptyToMax.index - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Processed, result) - assertDbEquals(repoId, TestDataMaxV2.index) - assertTimestampRecent(repoDao.getRepository(repoId)?.lastUpdated) - } + @Test + fun testFullIndexEmptyToMax() { + val repoId = repoDao.insertEmptyRepo("http://example.org", certificate = CERTIFICATE) + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-max/$SIGNED_FILE_NAME", + jsonPath = "index-max-v2.json", + indexFileV2 = TestDataEntry.emptyToMax.index, + ) + val result = indexUpdater.update(repo).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/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-mid/42.json", - indexFileV2 = TestDataEntry.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 testDiffMinToMid() { + val repoId = streamIndexV2IntoDb("index-min-v2.json") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-mid/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-mid/42.json", + indexFileV2 = TestDataEntry.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") - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-min/23.json", - indexFileV2 = TestDataEntry.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 testDiffEmptyToMin() { + val repoId = streamIndexV2IntoDb("index-empty-v2.json") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-min/23.json", + indexFileV2 = TestDataEntry.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") - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-max/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-max/1337.json", - indexFileV2 = TestDataEntry.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 testDiffMidToMax() { + val repoId = streamIndexV2IntoDb("index-mid-v2.json") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-max/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-max/1337.json", + indexFileV2 = TestDataEntry.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") - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-min/23.json", - indexFileV2 = TestDataEntry.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 testSameTimestampUnchanged() { + val repoId = streamIndexV2IntoDb("index-min-v2.json") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-min/23.json", + indexFileV2 = TestDataEntry.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") - val repo = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-min/23.json", - indexFileV2 = TestDataEntry.emptyToMin.diffs["23"] ?: fail() - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Unchanged, result) - assertDbEquals(repoId, TestDataMidV2.index) - } + @Test + fun testHigherTimestampUnchanged() { + val repoId = streamIndexV2IntoDb("index-mid-v2.json") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-min/23.json", + indexFileV2 = TestDataEntry.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") - // 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/$SIGNED_FILE_NAME", - jsonPath = "index-min-v2.json", - indexFileV2 = TestDataEntry.emptyToMin.index - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Processed, result) - assertDbEquals(repoId, TestDataMinV2.index) - } + @Test + fun testNoDiffFoundIndexFallback() { + val repoId = streamIndexV2IntoDb("index-empty-v2.json") + // 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/$SIGNED_FILE_NAME", + jsonPath = "index-min-v2.json", + indexFileV2 = TestDataEntry.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/$SIGNED_FILE_NAME", - jsonPath = "index-min-v2.json", - indexFileV2 = TestDataEntry.emptyToMin.index - ) - val result = indexUpdater.update(repo) - assertIs(result) - assertIs(result.e) - } + @Test + fun testWrongFingerprint() { + val repoId = repoDao.insertEmptyRepo("http://example.org") + val repo = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "index-min-v2.json", + indexFileV2 = TestDataEntry.emptyToMin.index, + ) + val result = indexUpdater.update(repo) + assertIs(result) + assertIs(result.e) + } - /** - * 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/$SIGNED_FILE_NAME", - jsonPath = "index-mid-v2.json", - indexFileV2 = TestDataEntry.emptyToMid.index, - ) - val result = indexUpdater.update(repo).noError() - assertEquals(IndexUpdateResult.Processed, result) - assertDbEquals(repoId, TestDataMidV2.index) + /** + * 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/$SIGNED_FILE_NAME", + jsonPath = "index-mid-v2.json", + indexFileV2 = TestDataEntry.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) - } + // check that format version got upgraded + val updatedRepo = repoDao.getRepository(repoId) ?: fail() + assertEquals(TWO, updatedRepo.formatVersion) + } - @Test - @OptIn(DelicateCoroutinesApi::class) - fun concurrentUpdateTest() { - val db = spyk(db) // spy on the DB, so we can mock a call to aid in concurrency - indexUpdater = IndexV2Updater( - database = db, - tempFileProvider = tempFileProvider, - downloaderFactory = downloaderFactory, - compatibilityChecker = compatibilityChecker, - ) - val repoId = streamIndexV2IntoDb("index-empty-v2.json") - val repo1 = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-min/23.json", - indexFileV2 = TestDataEntry.emptyToMin.diffs["23"] ?: fail() - ) - val repo2 = prepareUpdate( - repoId = repoId, - entryPath = "diff-empty-min/$SIGNED_FILE_NAME", - jsonPath = "diff-empty-min/23.json", - indexFileV2 = TestDataEntry.emptyToMin.diffs["23"] ?: fail() - ) - val latch = CountDownLatch(1) - val runSlot = slot() - every { db.runInTransaction(capture(runSlot)) } answers { - runSlot.captured.run() - latch.countDown() - } andThenAnswer { - latch.await() - runSlot.captured.run() - } - runBlocking { - GlobalScope.async { - val result1 = indexUpdater.update(repo1).noError() - assertEquals(IndexUpdateResult.Processed, result1) + @Test + @OptIn(DelicateCoroutinesApi::class) + fun concurrentUpdateTest() { + val db = spyk(db) // spy on the DB, so we can mock a call to aid in concurrency + indexUpdater = + IndexV2Updater( + database = db, + tempFileProvider = tempFileProvider, + downloaderFactory = downloaderFactory, + compatibilityChecker = compatibilityChecker, + ) + val repoId = streamIndexV2IntoDb("index-empty-v2.json") + val repo1 = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-min/23.json", + indexFileV2 = TestDataEntry.emptyToMin.diffs["23"] ?: fail(), + ) + val repo2 = + prepareUpdate( + repoId = repoId, + entryPath = "diff-empty-min/$SIGNED_FILE_NAME", + jsonPath = "diff-empty-min/23.json", + indexFileV2 = TestDataEntry.emptyToMin.diffs["23"] ?: fail(), + ) + val latch = CountDownLatch(1) + val runSlot = slot() + every { db.runInTransaction(capture(runSlot)) } answers + { + runSlot.captured.run() + latch.countDown() + } andThenAnswer + { + latch.await() + runSlot.captured.run() + } + runBlocking { + GlobalScope.async { + val result1 = indexUpdater.update(repo1).noError() + assertEquals(IndexUpdateResult.Processed, result1) - val entryFile = tmpFolder.newFile() - val indexFile = tmpFolder.newFile() - assets.open("diff-empty-min/$SIGNED_FILE_NAME").use { inputStream -> - entryFile.outputStream().use { inputStream.copyTo(it) } - } - assets.open("diff-empty-min/23.json").use { inputStream -> - indexFile.outputStream().use { inputStream.copyTo(it) } - } - every { - tempFileProvider.createTempFile(any()) - } returnsMany listOf(entryFile, indexFile) - - val result2 = indexUpdater.update(repo2) - assertIs(result2) - assertIs(result2.e) - }.await() - } - } - - private fun prepareUpdate( - repoId: Long, - entryPath: String, - jsonPath: String, - indexFileV2: EntryFileV2, - ): Repository { - val entryFileV2 = FileV2.fromPath("/$SIGNED_FILE_NAME") - val entryFile = tmpFolder.newFile() - val indexFile = tmpFolder.newFile() - val repo = repoDao.getRepository(repoId) ?: fail() - val entryUri = Uri.parse("${repo.address}/$SIGNED_FILE_NAME") - val indexUri = Uri.parse("${repo.address}/${indexFileV2.name.trimStart('/')}") - - assets.open(entryPath).use { inputStream -> + val entryFile = tmpFolder.newFile() + val indexFile = tmpFolder.newFile() + getRes("diff-empty-min/$SIGNED_FILE_NAME").use { inputStream -> entryFile.outputStream().use { inputStream.copyTo(it) } - } - assets.open(jsonPath).use { inputStream -> + } + getRes("diff-empty-min/23.json").use { inputStream -> indexFile.outputStream().use { inputStream.copyTo(it) } + } + every { tempFileProvider.createTempFile(any()) } returnsMany listOf(entryFile, indexFile) + + val result2 = indexUpdater.update(repo2) + assertIs(result2) + assertIs(result2.e) } + .await() + } + } - every { tempFileProvider.createTempFile(any()) } returnsMany listOf(entryFile, indexFile) - every { - downloaderFactory.createWithTryFirstMirror(repo, entryUri, entryFileV2, any()) - } returns downloader - every { downloader.download() } just Runs - every { - downloaderFactory.createWithTryFirstMirror(repo, indexUri, indexFileV2, any()) - } returns downloader - every { downloader.download() } just Runs + private fun prepareUpdate( + repoId: Long, + entryPath: String, + jsonPath: String, + indexFileV2: EntryFileV2, + ): Repository { + val entryFileV2 = FileV2.fromPath("/$SIGNED_FILE_NAME") + val entryFile = tmpFolder.newFile() + val indexFile = tmpFolder.newFile() + val repo = repoDao.getRepository(repoId) ?: fail() + val entryUri = Uri.parse("${repo.address}/$SIGNED_FILE_NAME") + val indexUri = Uri.parse("${repo.address}/${indexFileV2.name.trimStart('/')}") - return repo + getRes(entryPath).use { inputStream -> + entryFile.outputStream().use { inputStream.copyTo(it) } + } + getRes(jsonPath).use { inputStream -> + indexFile.outputStream().use { inputStream.copyTo(it) } } - /** - * Easier for debugging, if we throw the index error. - */ - private fun IndexUpdateResult.noError(): IndexUpdateResult { - if (this is IndexUpdateResult.Error) throw e - return this - } + every { tempFileProvider.createTempFile(any()) } returnsMany listOf(entryFile, indexFile) + every { downloaderFactory.createWithTryFirstMirror(repo, entryUri, entryFileV2, any()) } returns + downloader + every { downloader.download() } just Runs + every { downloaderFactory.createWithTryFirstMirror(repo, indexUri, indexFileV2, any()) } returns + downloader + every { downloader.download() } 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 + } } diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt index 0f4b2c1cd..db05d980b 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -37,8 +37,8 @@ import org.fdroid.database.Repository import org.fdroid.database.RepositoryDaoInt import org.fdroid.download.DownloaderFactory import org.fdroid.download.HttpManager -import org.fdroid.download.HttpManager.Companion.isInvalidHttpUrl import org.fdroid.download.NotFoundException +import org.fdroid.download.isInvalidHttpUrl import org.fdroid.index.IndexFormatVersion import org.fdroid.index.IndexUpdateResult import org.fdroid.index.RepoUpdater diff --git a/libs/database/src/test/java/org/fdroid/database/ConvertersTest.kt b/libs/database/src/test/java/org/fdroid/database/ConvertersTest.kt index c1cc7abf6..bc8ad4fa1 100644 --- a/libs/database/src/test/java/org/fdroid/database/ConvertersTest.kt +++ b/libs/database/src/test/java/org/fdroid/database/ConvertersTest.kt @@ -1,11 +1,11 @@ package org.fdroid.database -import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import org.fdroid.test.TestRepoUtils.getRandomLocalizedFileV2 import org.fdroid.test.TestUtils.getRandomList import org.fdroid.test.TestUtils.getRandomString +import org.junit.Test internal class ConvertersTest { diff --git a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt index b04dc3c78..b4a94ab09 100644 --- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt +++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt @@ -1,7 +1,6 @@ package org.fdroid.repo import android.content.Context -import android.content.res.AssetManager import android.net.Uri import android.os.UserManager import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES @@ -68,6 +67,7 @@ import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 import org.fdroid.test.TestUtils.decodeHex import org.fdroid.test.TestUtils.getRandomString +import org.fdroid.test.TestUtils.getRes import org.fdroid.test.VerifierConstants import org.junit.Rule import org.junit.Test @@ -90,7 +90,6 @@ internal class RepoAdderTest { private val digest = mockk() private val repoAdder: RepoAdder - private val assets: AssetManager = context.resources.assets private val localeList = LocaleListCompat.getDefault() init { @@ -486,9 +485,7 @@ internal class RepoAdderTest { every { downloader.download() } answers { jarFile.outputStream().use { outputStream -> - assets.open("diff-empty-min/entry.jar").use { inputStream -> - inputStream.copyTo(outputStream) - } + getRes("diff-empty-min/entry.jar").use { inputStream -> inputStream.copyTo(outputStream) } } } coEvery { @@ -601,9 +598,7 @@ internal class RepoAdderTest { every { downloader.download() } answers { jarFile.outputStream().use { outputStream -> - assets.open("diff-empty-min/entry.jar").use { inputStream -> - inputStream.copyTo(outputStream) - } + getRes("diff-empty-min/entry.jar").use { inputStream -> inputStream.copyTo(outputStream) } } } coEvery { @@ -662,7 +657,7 @@ internal class RepoAdderTest { every { downloader.download() } answers { jarFile.outputStream().use { outputStream -> - assets.open("guardianproject_entry.jar").use { inputStream -> + getRes("guardianproject_entry.jar").use { inputStream -> inputStream.copyTo(outputStream) } } @@ -737,7 +732,7 @@ internal class RepoAdderTest { every { downloaderV1.download() } answers { jarFile.outputStream().use { outputStream -> - assets.open("testy.at.or.at_index-v1.jar").use { inputStream -> + getRes("testy.at.or.at_index-v1.jar").use { inputStream -> inputStream.copyTo(outputStream) } } @@ -898,7 +893,7 @@ internal class RepoAdderTest { val jarFile = folder.newFile() - val indexInputStream = assets.open(indexFile) + val indexInputStream = getRes(indexFile) val indexDigestStream = DigestInputStream(indexInputStream, digest) every { tempFileProvider.createTempFile(any()) } returns jarFile @@ -914,7 +909,7 @@ internal class RepoAdderTest { every { downloader.download() } answers { jarFile.outputStream().use { outputStream -> - assets.open(entryJar).use { inputStream -> inputStream.copyTo(outputStream) } + getRes(entryJar).use { inputStream -> inputStream.copyTo(outputStream) } } } coEvery { diff --git a/libs/download/api/jvm/download.api b/libs/download/api/jvm/download.api new file mode 100644 index 000000000..3460c4c11 --- /dev/null +++ b/libs/download/api/jvm/download.api @@ -0,0 +1,137 @@ +public abstract interface class org/fdroid/download/BytesReceiver { + public abstract fun receive ([BLjava/lang/Long;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class org/fdroid/download/CustomDns { +} + +public final class org/fdroid/download/DownloadRequest { + public fun (Ljava/lang/String;Ljava/util/List;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/fdroid/IndexFile;Ljava/util/List;)V + public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;)V + public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;)V + public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;)V + public fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)V + public synthetic fun (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lorg/fdroid/IndexFile; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/net/Proxy; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Lorg/fdroid/download/Mirror; + public final fun copy (Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;)Lorg/fdroid/download/DownloadRequest; + public static synthetic fun copy$default (Lorg/fdroid/download/DownloadRequest;Lorg/fdroid/IndexFile;Ljava/util/List;Ljava/net/Proxy;Ljava/lang/String;Ljava/lang/String;Lorg/fdroid/download/Mirror;ILjava/lang/Object;)Lorg/fdroid/download/DownloadRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getHasCredentials ()Z + public final fun getIndexFile ()Lorg/fdroid/IndexFile; + public final fun getMirrors ()Ljava/util/List; + public final fun getPassword ()Ljava/lang/String; + public final fun getProxy ()Ljava/net/Proxy; + public final fun getTryFirstMirror ()Lorg/fdroid/download/Mirror; + public final fun getUsername ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/fdroid/download/HeadInfo { + public fun (ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V + public final fun component1 ()Z + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/String; + public final fun copy (ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/download/HeadInfo; + public static synthetic fun copy$default (Lorg/fdroid/download/HeadInfo;ZLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/download/HeadInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getContentLength ()Ljava/lang/Long; + public final fun getETag ()Ljava/lang/String; + public final fun getETagChanged ()Z + public final fun getLastModified ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class org/fdroid/download/HttpManager { + public static final field Companion Lorg/fdroid/download/HttpManager$Companion; + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;Lorg/fdroid/download/MirrorParameterManager;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;Lorg/fdroid/download/MirrorParameterManager;Z)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;Lio/ktor/client/engine/HttpClientEngineFactory;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lorg/fdroid/download/CustomDns;Lorg/fdroid/download/MirrorParameterManager;ZLorg/fdroid/download/MirrorChooser;Lio/ktor/client/engine/HttpClientEngineFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun get (Lorg/fdroid/download/DownloadRequest;Ljava/lang/Long;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun get (Lorg/fdroid/download/DownloadRequest;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun get$default (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/lang/Long;Lorg/fdroid/download/BytesReceiver;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun head (Lorg/fdroid/download/DownloadRequest;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun head$default (Lorg/fdroid/download/HttpManager;Lorg/fdroid/download/DownloadRequest;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun post (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun post$default (Lorg/fdroid/download/HttpManager;Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class org/fdroid/download/HttpManager$Companion { +} + +public final class org/fdroid/download/Mirror { + public static final field Companion Lorg/fdroid/download/Mirror$Companion; + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Z + public final fun copy (Ljava/lang/String;Ljava/lang/String;Z)Lorg/fdroid/download/Mirror; + public static synthetic fun copy$default (Lorg/fdroid/download/Mirror;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/fdroid/download/Mirror; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromStrings (Ljava/util/List;)Ljava/util/List; + public final fun getBaseUrl ()Ljava/lang/String; + public final fun getCountryCode ()Ljava/lang/String; + public final fun getFDroidLinkUrl (Ljava/lang/String;)Ljava/lang/String; + public final fun getUrl ()Lio/ktor/http/Url; + public final fun getUrl (Ljava/lang/String;)Lio/ktor/http/Url; + public fun hashCode ()I + public final fun isHttp ()Z + public final fun isIpfsGateway ()Z + public final fun isLocal ()Z + public final fun isOnion ()Z + public fun toString ()Ljava/lang/String; +} + +public final class org/fdroid/download/Mirror$Companion { + public final fun fromStrings (Ljava/util/List;)Ljava/util/List; +} + +public abstract interface class org/fdroid/download/MirrorChooser { + public abstract fun mirrorRequest (Lorg/fdroid/download/DownloadRequest;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun orderMirrors (Lorg/fdroid/download/DownloadRequest;)Ljava/util/List; +} + +public abstract interface class org/fdroid/download/MirrorParameterManager { + public abstract fun getCurrentLocation ()Ljava/lang/String; + public abstract fun getMirrorErrorCount (Ljava/lang/String;)I + public abstract fun incrementMirrorErrorCount (Ljava/lang/String;)V + public abstract fun preferForeignMirrors ()Z + public abstract fun shouldRetryRequest (Ljava/lang/String;)Z +} + +public final class org/fdroid/download/NoResumeException : java/lang/Exception { + public fun ()V +} + +public final class org/fdroid/download/NotFoundException : java/lang/Exception { + public fun ()V + public fun (Ljava/lang/Throwable;)V + public synthetic fun (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract interface class org/fdroid/fdroid/ProgressListener { + public abstract fun onProgress (JJ)V +} + diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts index cceebda02..227e2ce27 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -11,12 +11,13 @@ kotlin { explicitApi() @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) abiValidation { enabled = true } + + jvm() android { namespace = "org.fdroid.download" compileSdk = libs.versions.compileSdk.get().toInt() minSdk = 21 - withJava() compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } withHostTest { packaging { resources.excludes.add("META-INF/*") } } @@ -24,7 +25,7 @@ kotlin { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" instrumentationRunnerArguments["disableAnalytics"] = "true" } - + androidResources { enable = true } lint { checkReleaseBuilds = false abortOnError = true @@ -51,7 +52,6 @@ kotlin { implementation(libs.mockk) } } - // JVM is disabled for now, because Android app is including it instead of Android library jvmMain { dependencies { implementation(libs.ktor.client.cio) } } jvmTest { dependencies { implementation(libs.junit) } } androidMain { diff --git a/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpManager.kt b/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpManager.kt index 1ea2f17fa..e10be9d3a 100644 --- a/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpManager.kt +++ b/libs/download/src/androidMain/kotlin/org/fdroid/download/HttpManager.kt @@ -14,6 +14,7 @@ import okhttp3.ConnectionSpec.Companion.CLEARTEXT import okhttp3.ConnectionSpec.Companion.MODERN_TLS import okhttp3.ConnectionSpec.Companion.RESTRICTED_TLS import okhttp3.Dns +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.internal.tls.OkHostnameVerifier internal actual fun getHttpClientEngineFactory(customDns: Dns?): HttpClientEngineFactory<*> { @@ -51,6 +52,10 @@ internal actual fun getHttpClientEngineFactory(customDns: Dns?): HttpClientEngin } } +public fun isInvalidHttpUrl(url: String): Boolean { + return url.toHttpUrlOrNull() == null +} + public suspend fun HttpManager.getInputStream(request: DownloadRequest): InputStream { return getChannel(request).toInputStream() } diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt index bc0274fc6..392ab8ca4 100644 --- a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt +++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt @@ -60,8 +60,6 @@ constructor( internal val log = KotlinLogging.logger {} internal const val READ_BUFFER = 8 * 1024 private const val TIMEOUT_MILLIS_HIGH = 300_000L - - public fun isInvalidHttpUrl(url: String): Boolean = url.toHttpUrlOrNull() == null } private var httpClient = getNewHttpClient(proxyConfig) diff --git a/libs/index/api/jvm/index.api b/libs/index/api/jvm/index.api new file mode 100644 index 000000000..09ead77a6 --- /dev/null +++ b/libs/index/api/jvm/index.api @@ -0,0 +1,1119 @@ +public final class org/fdroid/index/IndexConverter { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun toIndexV2 (Lorg/fdroid/index/v1/IndexV1;)Lorg/fdroid/index/v2/IndexV2; +} + +public final class org/fdroid/index/IndexConverterKt { + public static final field RELEASE_CHANNEL_BETA Ljava/lang/String; + public static final fun getV1ReleaseChannels ()Ljava/util/Map; +} + +public final class org/fdroid/index/IndexParser { + public static final field INSTANCE Lorg/fdroid/index/IndexParser; + public static final fun getJson ()Lkotlinx/serialization/json/Json; + public static final fun parseEntry (Ljava/lang/String;)Lorg/fdroid/index/v2/Entry; + public static final fun parseV1 (Ljava/lang/String;)Lorg/fdroid/index/v1/IndexV1; + public static final fun parseV2 (Ljava/lang/String;)Lorg/fdroid/index/v2/IndexV2; +} + +public final class org/fdroid/index/v1/AppV1 { + public static final field Companion Lorg/fdroid/index/v1/AppV1$Companion; + public fun (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;)V + public synthetic fun (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component10 ()Ljava/lang/String; + public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Ljava/lang/String; + public final fun component13 ()Ljava/lang/String; + public final fun component14 ()Ljava/lang/String; + public final fun component15 ()Ljava/lang/String; + public final fun component16 ()Ljava/lang/String; + public final fun component17 ()Ljava/lang/String; + public final fun component18 ()Ljava/lang/String; + public final fun component19 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/List; + public final fun component20 ()Ljava/lang/String; + public final fun component21 ()Ljava/lang/String; + public final fun component22 ()Ljava/lang/String; + public final fun component23 ()Ljava/lang/String; + public final fun component24 ()Ljava/lang/String; + public final fun component25 ()Ljava/lang/String; + public final fun component26 ()Ljava/lang/Long; + public final fun component27 ()Ljava/lang/String; + public final fun component28 ()Ljava/lang/String; + public final fun component29 ()Ljava/lang/Long; + public final fun component3 ()Ljava/lang/String; + public final fun component30 ()Ljava/util/Map; + public final fun component31 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;)Lorg/fdroid/index/v1/AppV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/AppV1;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/AppV1; + public fun equals (Ljava/lang/Object;)Z + public final fun getAdded ()Ljava/lang/Long; + public final fun getAllowedAPKSigningKeys ()Ljava/util/List; + public final fun getAntiFeatures ()Ljava/util/List; + public final fun getAuthorEmail ()Ljava/lang/String; + public final fun getAuthorName ()Ljava/lang/String; + public final fun getAuthorPhone ()Ljava/lang/String; + public final fun getAuthorWebSite ()Ljava/lang/String; + public final fun getBinaries ()Ljava/lang/String; + public final fun getBitcoin ()Ljava/lang/String; + public final fun getCategories ()Ljava/util/List; + public final fun getChangelog ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getDonate ()Ljava/lang/String; + public final fun getFlattrID ()Ljava/lang/String; + public final fun getIcon ()Ljava/lang/String; + public final fun getIssueTracker ()Ljava/lang/String; + public final fun getLastUpdated ()Ljava/lang/Long; + public final fun getLiberapay ()Ljava/lang/String; + public final fun getLiberapayID ()Ljava/lang/String; + public final fun getLicense ()Ljava/lang/String; + public final fun getLitecoin ()Ljava/lang/String; + public final fun getLocalized ()Ljava/util/Map; + public final fun getName ()Ljava/lang/String; + public final fun getOpenCollective ()Ljava/lang/String; + public final fun getPackageName ()Ljava/lang/String; + public final fun getSourceCode ()Ljava/lang/String; + public final fun getSuggestedVersionCode ()Ljava/lang/String; + public final fun getSuggestedVersionName ()Ljava/lang/String; + public final fun getSummary ()Ljava/lang/String; + public final fun getTranslation ()Ljava/lang/String; + public final fun getWebSite ()Ljava/lang/String; + public fun hashCode ()I + public final fun toMetadataV2 (Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v2/MetadataV2; + public static synthetic fun toMetadataV2$default (Lorg/fdroid/index/v1/AppV1;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/MetadataV2; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/AppV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/AppV1$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/AppV1; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/AppV1;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/AppV1$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/IndexV1 { + public static final field Companion Lorg/fdroid/index/v1/IndexV1$Companion; + public fun (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;)V + public synthetic fun (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lorg/fdroid/index/v1/RepoV1; + public final fun component2 ()Lorg/fdroid/index/v1/Requests; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/util/Map; + public final fun copy (Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;)Lorg/fdroid/index/v1/IndexV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/IndexV1;Lorg/fdroid/index/v1/RepoV1;Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v1/IndexV1; + public fun equals (Ljava/lang/Object;)Z + public final fun getApps ()Ljava/util/List; + public final fun getPackages ()Ljava/util/Map; + public final fun getRepo ()Lorg/fdroid/index/v1/RepoV1; + public final fun getRequests ()Lorg/fdroid/index/v1/Requests; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/IndexV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/IndexV1$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/IndexV1; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/IndexV1;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/IndexV1$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class org/fdroid/index/v1/IndexV1StreamReceiver { + public abstract fun receive (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun receive (Ljava/lang/String;Lorg/fdroid/index/v2/MetadataV2;)V + public abstract fun receive (Lorg/fdroid/index/v2/RepoV2;J)V + public abstract fun updateAppMetadata (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun updateRepo (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V +} + +public final class org/fdroid/index/v1/Localized { + public static final field Companion Lorg/fdroid/index/v1/Localized$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component10 ()Ljava/util/List; + public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Ljava/lang/String; + public final fun component13 ()Ljava/lang/String; + public final fun component14 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v1/Localized; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/Localized;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v1/Localized; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/lang/String; + public final fun getFeatureGraphic ()Ljava/lang/String; + public final fun getIcon ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getPhoneScreenshots ()Ljava/util/List; + public final fun getPromoGraphic ()Ljava/lang/String; + public final fun getSevenInchScreenshots ()Ljava/util/List; + public final fun getSummary ()Ljava/lang/String; + public final fun getTenInchScreenshots ()Ljava/util/List; + public final fun getTvBanner ()Ljava/lang/String; + public final fun getTvScreenshots ()Ljava/util/List; + public final fun getVideo ()Ljava/lang/String; + public final fun getWearScreenshots ()Ljava/util/List; + public final fun getWhatsNew ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/Localized$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/Localized$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/Localized; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/Localized;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/Localized$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/PackageV1 { + public static final field Companion Lorg/fdroid/index/v1/PackageV1$Companion; + public fun (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V + public synthetic fun (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Long; + public final fun component10 ()Ljava/lang/String; + public final fun component11 ()J + public final fun component12 ()Ljava/lang/String; + public final fun component13 ()Ljava/util/List; + public final fun component14 ()Ljava/util/List; + public final fun component15 ()Ljava/lang/Long; + public final fun component16 ()Ljava/lang/String; + public final fun component17 ()Ljava/util/List; + public final fun component18 ()Ljava/util/List; + public final fun component19 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/Integer; + public final fun component6 ()Ljava/lang/Integer; + public final fun component7 ()Ljava/lang/Integer; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/String; + public final fun copy (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v1/PackageV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/PackageV1;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/PackageV1; + public fun equals (Ljava/lang/Object;)Z + public final fun getAdded ()Ljava/lang/Long; + public final fun getAntiFeatures ()Ljava/util/List; + public final fun getApkName ()Ljava/lang/String; + public final fun getFeatures ()Ljava/util/List; + public final fun getHash ()Ljava/lang/String; + public final fun getHashType ()Ljava/lang/String; + public final fun getMaxSdkVersion ()Ljava/lang/Integer; + public final fun getMinSdkVersion ()Ljava/lang/Integer; + public final fun getNativeCode ()Ljava/util/List; + public final fun getPackageName ()Ljava/lang/String; + public final fun getSig ()Ljava/lang/String; + public final fun getSigner ()Ljava/lang/String; + public final fun getSize ()J + public final fun getSrcName ()Ljava/lang/String; + public final fun getTargetSdkVersion ()Ljava/lang/Integer; + public final fun getUsesPermission ()Ljava/util/List; + public final fun getUsesPermission23 ()Ljava/util/List; + public final fun getVersionCode ()Ljava/lang/Long; + public final fun getVersionName ()Ljava/lang/String; + public fun hashCode ()I + public final fun toPackageVersionV2 (Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageVersionV2; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/PackageV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/PackageV1$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/PackageV1; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/PackageV1;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/PackageV1$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/PermissionV1 { + public static final field Companion Lorg/fdroid/index/v1/PermissionV1$Companion; + public fun (Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lorg/fdroid/index/v1/PermissionV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/PermissionV1;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/fdroid/index/v1/PermissionV1; + public fun equals (Ljava/lang/Object;)Z + public final fun getMaxSdk ()Ljava/lang/Integer; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/fdroid/index/v1/PermissionV1$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/RepoV1 { + public static final field Companion Lorg/fdroid/index/v1/RepoV1$Companion; + public fun (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()I + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/util/List; + public final fun copy (JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lorg/fdroid/index/v1/RepoV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/RepoV1;JILjava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/RepoV1; + public fun equals (Ljava/lang/Object;)Z + public final fun getAddress ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getIcon ()Ljava/lang/String; + public final fun getMaxAge ()Ljava/lang/Integer; + public final fun getMirrors ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getTimestamp ()J + public final fun getVersion ()I + public fun hashCode ()I + public final fun toRepoV2 (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/RepoV2; + public static synthetic fun toRepoV2$default (Lorg/fdroid/index/v1/RepoV1;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/RepoV2; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/RepoV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/RepoV1$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/RepoV1; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/RepoV1;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/RepoV1$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/Requests { + public static final field Companion Lorg/fdroid/index/v1/Requests$Companion; + public fun (Ljava/util/List;Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/util/List; + public final fun copy (Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v1/Requests; + public static synthetic fun copy$default (Lorg/fdroid/index/v1/Requests;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v1/Requests; + public fun equals (Ljava/lang/Object;)Z + public final fun getInstall ()Ljava/util/List; + public final fun getUninstall ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v1/Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v1/Requests$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v1/Requests; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v1/Requests;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v1/Requests$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/AntiFeatureV2 { + public static final field Companion Lorg/fdroid/index/v2/AntiFeatureV2$Companion; + public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/AntiFeatureV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/AntiFeatureV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/AntiFeatureV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/util/Map; + public final fun getIcon ()Ljava/util/Map; + public final fun getName ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/AntiFeatureV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/AntiFeatureV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/AntiFeatureV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/AntiFeatureV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/AntiFeatureV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/CategoryV2 { + public static final field Companion Lorg/fdroid/index/v2/CategoryV2$Companion; + public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/CategoryV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/CategoryV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/CategoryV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/util/Map; + public final fun getIcon ()Ljava/util/Map; + public final fun getName ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/CategoryV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/CategoryV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/CategoryV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/CategoryV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/CategoryV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/Entry { + public static final field Companion Lorg/fdroid/index/v2/Entry$Companion; + public fun (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;)V + public synthetic fun (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()J + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Lorg/fdroid/index/v2/EntryFileV2; + public final fun component5 ()Ljava/util/Map; + public final fun copy (JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;)Lorg/fdroid/index/v2/Entry; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/Entry;JJLjava/lang/Integer;Lorg/fdroid/index/v2/EntryFileV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/Entry; + public fun equals (Ljava/lang/Object;)Z + public final fun getDiff (J)Lorg/fdroid/index/v2/EntryFileV2; + public final fun getDiffs ()Ljava/util/Map; + public final fun getIndex ()Lorg/fdroid/index/v2/EntryFileV2; + public final fun getMaxAge ()Ljava/lang/Integer; + public final fun getTimestamp ()J + public final fun getVersion ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/Entry$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/Entry$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/Entry; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/Entry;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/Entry$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/EntryFileV2 : org/fdroid/IndexFile { + public static final field Companion Lorg/fdroid/index/v2/EntryFileV2$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;I)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()J + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()I + public final fun copy (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;I)Lorg/fdroid/index/v2/EntryFileV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/EntryFileV2;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;IILjava/lang/Object;)Lorg/fdroid/index/v2/EntryFileV2; + public fun equals (Ljava/lang/Object;)Z + public fun getIpfsCidV1 ()Ljava/lang/String; + public fun getName ()Ljava/lang/String; + public final fun getNumPackages ()I + public fun getSha256 ()Ljava/lang/String; + public fun getSize ()Ljava/lang/Long; + public fun hashCode ()I + public fun serialize ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/EntryFileV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/EntryFileV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/EntryFileV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/EntryFileV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/EntryFileV2$Companion { + public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/EntryFileV2; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FeatureV2 { + public static final field Companion Lorg/fdroid/index/v2/FeatureV2$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lorg/fdroid/index/v2/FeatureV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/FeatureV2;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FeatureV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/FeatureV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/FeatureV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FeatureV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FeatureV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FeatureV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FileV1 : org/fdroid/IndexFile { + public static final field Companion Lorg/fdroid/index/v2/FileV1$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/FileV1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FileV1; + public static final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1; + public fun equals (Ljava/lang/Object;)Z + public fun getIpfsCidV1 ()Ljava/lang/String; + public fun getName ()Ljava/lang/String; + public fun getSha256 ()Ljava/lang/String; + public fun getSize ()Ljava/lang/Long; + public fun hashCode ()I + public fun serialize ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/FileV1$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/FileV1$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FileV1; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FileV1;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FileV1$Companion { + public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV1; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FileV2 : org/fdroid/IndexFile { + public static final field Companion Lorg/fdroid/index/v2/FileV2$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/FileV2;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/FileV2; + public static final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromPath (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2; + public fun getIpfsCidV1 ()Ljava/lang/String; + public fun getName ()Ljava/lang/String; + public fun getSha256 ()Ljava/lang/String; + public fun getSize ()Ljava/lang/Long; + public fun hashCode ()I + public fun serialize ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/FileV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/FileV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/FileV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/FileV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/FileV2$Companion { + public final fun deserialize (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2; + public final fun fromPath (Ljava/lang/String;)Lorg/fdroid/index/v2/FileV2; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/IndexV2 { + public static final field Companion Lorg/fdroid/index/v2/IndexV2$Companion; + public fun (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;)V + public synthetic fun (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lorg/fdroid/index/v2/RepoV2; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;)Lorg/fdroid/index/v2/IndexV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/IndexV2;Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/IndexV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getPackages ()Ljava/util/Map; + public final fun getRepo ()Lorg/fdroid/index/v2/RepoV2; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V +} + +public final synthetic class org/fdroid/index/v2/IndexV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/IndexV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/IndexV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/IndexV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/IndexV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class org/fdroid/index/v2/IndexV2DiffStreamReceiver { + public abstract fun onStreamEnded ()V + public abstract fun receivePackageMetadataDiff (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public abstract fun receiveRepoDiff (JLkotlinx/serialization/json/JsonObject;)V + public abstract fun receiveVersionsDiff (Ljava/lang/String;Ljava/util/Map;)V +} + +public abstract interface class org/fdroid/index/v2/IndexV2StreamReceiver { + public abstract fun onStreamEnded ()V + public abstract fun receive (Ljava/lang/String;Lorg/fdroid/index/v2/PackageV2;)V + public abstract fun receive (Lorg/fdroid/index/v2/RepoV2;J)V +} + +public final class org/fdroid/index/v2/ManifestV2 : org/fdroid/index/v2/PackageManifest { + public static final field Companion Lorg/fdroid/index/v2/ManifestV2$Companion; + public fun (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()J + public final fun component3 ()Lorg/fdroid/index/v2/UsesSdkV2; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lorg/fdroid/index/v2/SignerV2; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Ljava/util/List; + public final fun component9 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lorg/fdroid/index/v2/ManifestV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/ManifestV2;Ljava/lang/String;JLorg/fdroid/index/v2/UsesSdkV2;Ljava/lang/Integer;Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lorg/fdroid/index/v2/ManifestV2; + public fun equals (Ljava/lang/Object;)Z + public fun getFeatureNames ()Ljava/util/List; + public final fun getFeatures ()Ljava/util/List; + public fun getMaxSdkVersion ()Ljava/lang/Integer; + public fun getMinSdkVersion ()Ljava/lang/Integer; + public fun getNativecode ()Ljava/util/List; + public final fun getSigner ()Lorg/fdroid/index/v2/SignerV2; + public fun getTargetSdkVersion ()Ljava/lang/Integer; + public final fun getUsesPermission ()Ljava/util/List; + public final fun getUsesPermissionSdk23 ()Ljava/util/List; + public final fun getUsesSdk ()Lorg/fdroid/index/v2/UsesSdkV2; + public final fun getVersionCode ()J + public final fun getVersionName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/ManifestV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/ManifestV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/ManifestV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/ManifestV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/ManifestV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/MetadataV2 { + public static final field Companion Lorg/fdroid/index/v2/MetadataV2$Companion; + public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component10 ()Ljava/lang/String; + public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Ljava/lang/String; + public final fun component13 ()Ljava/util/List; + public final fun component14 ()Ljava/lang/String; + public final fun component15 ()Ljava/lang/String; + public final fun component16 ()Ljava/lang/String; + public final fun component17 ()Ljava/lang/String; + public final fun component18 ()Ljava/util/List; + public final fun component19 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun component20 ()Ljava/lang/String; + public final fun component21 ()Ljava/lang/String; + public final fun component22 ()Ljava/lang/String; + public final fun component23 ()Ljava/lang/String; + public final fun component24 ()Ljava/lang/String; + public final fun component25 ()Ljava/util/Map; + public final fun component26 ()Ljava/util/Map; + public final fun component27 ()Ljava/util/Map; + public final fun component28 ()Ljava/util/Map; + public final fun component29 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/Map; + public final fun component30 ()Lorg/fdroid/index/v2/Screenshots; + public final fun component4 ()J + public final fun component5 ()J + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Ljava/lang/String; + public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;)Lorg/fdroid/index/v2/MetadataV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lorg/fdroid/index/v2/Screenshots;ILjava/lang/Object;)Lorg/fdroid/index/v2/MetadataV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getAdded ()J + public final fun getAuthorEmail ()Ljava/lang/String; + public final fun getAuthorName ()Ljava/lang/String; + public final fun getAuthorPhone ()Ljava/lang/String; + public final fun getAuthorWebSite ()Ljava/lang/String; + public final fun getBitcoin ()Ljava/lang/String; + public final fun getCategories ()Ljava/util/List; + public final fun getChangelog ()Ljava/lang/String; + public final fun getDescription ()Ljava/util/Map; + public final fun getDonate ()Ljava/util/List; + public final fun getFeatureGraphic ()Ljava/util/Map; + public final fun getFlattrID ()Ljava/lang/String; + public final fun getIcon ()Ljava/util/Map; + public final fun getIssueTracker ()Ljava/lang/String; + public final fun getLastUpdated ()J + public final fun getLiberapay ()Ljava/lang/String; + public final fun getLiberapayID ()Ljava/lang/String; + public final fun getLicense ()Ljava/lang/String; + public final fun getLitecoin ()Ljava/lang/String; + public final fun getName ()Ljava/util/Map; + public final fun getOpenCollective ()Ljava/lang/String; + public final fun getPreferredSigner ()Ljava/lang/String; + public final fun getPromoGraphic ()Ljava/util/Map; + public final fun getScreenshots ()Lorg/fdroid/index/v2/Screenshots; + public final fun getSourceCode ()Ljava/lang/String; + public final fun getSummary ()Ljava/util/Map; + public final fun getTranslation ()Ljava/lang/String; + public final fun getTvBanner ()Ljava/util/Map; + public final fun getVideo ()Ljava/util/Map; + public final fun getWebSite ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V +} + +public final synthetic class org/fdroid/index/v2/MetadataV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/MetadataV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/MetadataV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/MetadataV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/MetadataV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/MirrorV2 { + public static final field Companion Lorg/fdroid/index/v2/MirrorV2$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lorg/fdroid/index/v2/MirrorV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/MirrorV2;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/fdroid/index/v2/MirrorV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getCountryCode ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/MirrorV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/MirrorV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/MirrorV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/MirrorV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/MirrorV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class org/fdroid/index/v2/PackageManifest { + public abstract fun getFeatureNames ()Ljava/util/List; + public abstract fun getMaxSdkVersion ()Ljava/lang/Integer; + public abstract fun getMinSdkVersion ()Ljava/lang/Integer; + public abstract fun getNativecode ()Ljava/util/List; + public abstract fun getTargetSdkVersion ()Ljava/lang/Integer; +} + +public final class org/fdroid/index/v2/PackageV2 { + public static final field Companion Lorg/fdroid/index/v2/PackageV2$Companion; + public fun (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;)V + public synthetic fun (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lorg/fdroid/index/v2/MetadataV2; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/PackageV2;Lorg/fdroid/index/v2/MetadataV2;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getMetadata ()Lorg/fdroid/index/v2/MetadataV2; + public final fun getVersions ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V +} + +public final synthetic class org/fdroid/index/v2/PackageV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/PackageV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PackageV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PackageV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/PackageV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/PackageV2Kt { + public static final field ANTI_FEATURE_KNOWN_VULNERABILITY Ljava/lang/String; +} + +public abstract interface class org/fdroid/index/v2/PackageVersion { + public abstract fun getAdded ()J + public abstract fun getHasKnownVulnerability ()Z + public abstract fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest; + public abstract fun getReleaseChannels ()Ljava/util/List; + public abstract fun getSigner ()Lorg/fdroid/index/v2/SignerV2; + public abstract fun getSize ()Ljava/lang/Long; + public abstract fun getVersionCode ()J + public abstract fun getVersionName ()Ljava/lang/String; +} + +public final class org/fdroid/index/v2/PackageVersionV2 : org/fdroid/index/v2/PackageVersion { + public static final field Companion Lorg/fdroid/index/v2/PackageVersionV2$Companion; + public fun (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()Lorg/fdroid/index/v2/FileV1; + public final fun component3 ()Lorg/fdroid/index/v2/FileV2; + public final fun component4 ()Lorg/fdroid/index/v2/ManifestV2; + public final fun component5 ()Ljava/util/List; + public final fun component6 ()Ljava/util/Map; + public final fun component7 ()Ljava/util/Map; + public final fun copy (JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/PackageVersionV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/PackageVersionV2;JLorg/fdroid/index/v2/FileV1;Lorg/fdroid/index/v2/FileV2;Lorg/fdroid/index/v2/ManifestV2;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/PackageVersionV2; + public fun equals (Ljava/lang/Object;)Z + public fun getAdded ()J + public final fun getAntiFeatures ()Ljava/util/Map; + public final fun getFile ()Lorg/fdroid/index/v2/FileV1; + public fun getHasKnownVulnerability ()Z + public final fun getManifest ()Lorg/fdroid/index/v2/ManifestV2; + public fun getPackageManifest ()Lorg/fdroid/index/v2/PackageManifest; + public fun getReleaseChannels ()Ljava/util/List; + public fun getSigner ()Lorg/fdroid/index/v2/SignerV2; + public fun getSize ()Ljava/lang/Long; + public final fun getSrc ()Lorg/fdroid/index/v2/FileV2; + public fun getVersionCode ()J + public fun getVersionName ()Ljava/lang/String; + public final fun getWhatsNew ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V +} + +public final synthetic class org/fdroid/index/v2/PackageVersionV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/PackageVersionV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PackageVersionV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PackageVersionV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/PackageVersionV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/PermissionV2 { + public static final field Companion Lorg/fdroid/index/v2/PermissionV2$Companion; + public fun (Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/Integer;)Lorg/fdroid/index/v2/PermissionV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/PermissionV2;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lorg/fdroid/index/v2/PermissionV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getMaxSdkVersion ()Ljava/lang/Integer; + public final fun getName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/PermissionV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/PermissionV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/PermissionV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/PermissionV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/PermissionV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/ReleaseChannelV2 { + public static final field Companion Lorg/fdroid/index/v2/ReleaseChannelV2$Companion; + public fun (Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/ReleaseChannelV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/ReleaseChannelV2;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/ReleaseChannelV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescription ()Ljava/util/Map; + public final fun getName ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/ReleaseChannelV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/ReleaseChannelV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/ReleaseChannelV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/ReleaseChannelV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/ReleaseChannelV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/RepoV2 { + public static final field Companion Lorg/fdroid/index/v2/RepoV2$Companion; + public fun (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component10 ()Ljava/util/Map; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/util/Map; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()J + public final fun component8 ()Ljava/util/Map; + public final fun component9 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/RepoV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/RepoV2;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;JLjava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/RepoV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getAddress ()Ljava/lang/String; + public final fun getAntiFeatures ()Ljava/util/Map; + public final fun getCategories ()Ljava/util/Map; + public final fun getDescription ()Ljava/util/Map; + public final fun getIcon ()Ljava/util/Map; + public final fun getMirrors ()Ljava/util/List; + public final fun getName ()Ljava/util/Map; + public final fun getReleaseChannels ()Ljava/util/Map; + public final fun getTimestamp ()J + public final fun getWebBaseUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun walkFiles (Lkotlin/jvm/functions/Function1;)V +} + +public final synthetic class org/fdroid/index/v2/RepoV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/RepoV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/RepoV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/RepoV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/RepoV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/Screenshots { + public static final field Companion Lorg/fdroid/index/v2/Screenshots$Companion; + public fun ()V + public fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public synthetic fun (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/Map; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/Map; + public final fun component4 ()Ljava/util/Map; + public final fun component5 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lorg/fdroid/index/v2/Screenshots; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/Screenshots;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lorg/fdroid/index/v2/Screenshots; + public fun equals (Ljava/lang/Object;)Z + public final fun getPhone ()Ljava/util/Map; + public final fun getSevenInch ()Ljava/util/Map; + public final fun getTenInch ()Ljava/util/Map; + public final fun getTv ()Ljava/util/Map; + public final fun getWear ()Ljava/util/Map; + public fun hashCode ()I + public final fun isNull ()Z + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/Screenshots$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/Screenshots$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/Screenshots; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/Screenshots;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/Screenshots$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/SignerV2 { + public static final field Companion Lorg/fdroid/index/v2/SignerV2$Companion; + public fun (Ljava/util/List;Z)V + public synthetic fun (Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Z + public final fun copy (Ljava/util/List;Z)Lorg/fdroid/index/v2/SignerV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/SignerV2;Ljava/util/List;ZILjava/lang/Object;)Lorg/fdroid/index/v2/SignerV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getHasMultipleSigners ()Z + public final fun getSha256 ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/SignerV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/SignerV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/SignerV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/SignerV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/SignerV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/UsesSdkV2 { + public static final field Companion Lorg/fdroid/index/v2/UsesSdkV2$Companion; + public fun (II)V + public final fun component1 ()I + public final fun component2 ()I + public final fun copy (II)Lorg/fdroid/index/v2/UsesSdkV2; + public static synthetic fun copy$default (Lorg/fdroid/index/v2/UsesSdkV2;IIILjava/lang/Object;)Lorg/fdroid/index/v2/UsesSdkV2; + public fun equals (Ljava/lang/Object;)Z + public final fun getMinSdkVersion ()I + public final fun getTargetSdkVersion ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class org/fdroid/index/v2/UsesSdkV2$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/fdroid/index/v2/UsesSdkV2$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/fdroid/index/v2/UsesSdkV2; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/fdroid/index/v2/UsesSdkV2;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/fdroid/index/v2/UsesSdkV2$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + diff --git a/libs/index/build.gradle.kts b/libs/index/build.gradle.kts index 518b1c333..9125011f6 100644 --- a/libs/index/build.gradle.kts +++ b/libs/index/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class) abiValidation { enabled = true } + jvm() android { namespace = "org.fdroid.index" compileSdk = libs.versions.compileSdk.get().toInt() @@ -26,7 +27,6 @@ kotlin { } } - withJava() compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } withHostTest { @@ -54,7 +54,6 @@ kotlin { implementation(libs.goncalossilva.resources) } } - // JVM is disabled for now, because Android app is including it instead of Android library jvmMain { dependencies {} } jvmTest { dependencies { implementation(libs.junit) } } androidMain { diff --git a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v1/IndexV1StreamProcessorTest.kt b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v1/IndexV1StreamProcessorTest.kt index ddfb6d0e6..a8c68f516 100644 --- a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v1/IndexV1StreamProcessorTest.kt +++ b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v1/IndexV1StreamProcessorTest.kt @@ -9,7 +9,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNull import kotlin.test.fail import kotlinx.serialization.SerializationException -import org.fdroid.index.ASSET_PATH import org.fdroid.index.v2.AntiFeatureV2 import org.fdroid.index.v2.CategoryV2 import org.fdroid.index.v2.IndexV2 @@ -22,6 +21,7 @@ import org.fdroid.test.TestDataEmptyV2 import org.fdroid.test.TestDataMaxV2 import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 +import org.fdroid.test.TestUtils.ASSET_PATH import org.fdroid.test.v1compat import org.junit.Test diff --git a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/IndexV2FullStreamProcessorTest.kt b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/IndexV2FullStreamProcessorTest.kt index b201a69a5..224415880 100644 --- a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/IndexV2FullStreamProcessorTest.kt +++ b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/IndexV2FullStreamProcessorTest.kt @@ -10,11 +10,11 @@ import kotlin.test.assertTrue import kotlin.test.fail import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException -import org.fdroid.index.ASSET_PATH import org.fdroid.test.TestDataEmptyV2 import org.fdroid.test.TestDataMaxV2 import org.fdroid.test.TestDataMidV2 import org.fdroid.test.TestDataMinV2 +import org.fdroid.test.TestUtils.ASSET_PATH import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder diff --git a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/ReflectionDifferTest.kt b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/ReflectionDifferTest.kt index dde6aab45..d23ea0dab 100644 --- a/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/ReflectionDifferTest.kt +++ b/libs/index/src/androidHostTest/kotlin/org/fdroid/index/v2/ReflectionDifferTest.kt @@ -13,7 +13,6 @@ import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject -import org.fdroid.index.ASSET_PATH import org.fdroid.index.IndexParser import org.fdroid.index.IndexParser.json import org.fdroid.index.parseV2 @@ -22,6 +21,7 @@ import org.fdroid.test.DiffUtils.cleanMetadata import org.fdroid.test.DiffUtils.cleanRepo import org.fdroid.test.DiffUtils.cleanVersion import org.fdroid.test.LOCALE +import org.fdroid.test.TestUtils.ASSET_PATH internal class ReflectionDifferTest { diff --git a/libs/index/src/commonTest/kotlin/org/fdroid/index/IndexConverterTest.kt b/libs/index/src/commonTest/kotlin/org/fdroid/index/IndexConverterTest.kt index 27189fbbe..de86ec4ae 100644 --- a/libs/index/src/commonTest/kotlin/org/fdroid/index/IndexConverterTest.kt +++ b/libs/index/src/commonTest/kotlin/org/fdroid/index/IndexConverterTest.kt @@ -12,28 +12,26 @@ import org.fdroid.test.TestDataMinV2 import org.fdroid.test.TestUtils.sorted import org.fdroid.test.v1compat -internal const val ASSET_PATH = "../sharedTest/src/main/assets" - internal class IndexConverterTest { @Test fun testEmpty() { - testConversation("$ASSET_PATH/index-empty-v1.json", TestDataEmptyV2.index.v1compat()) + testConversation("index-empty-v1.json", TestDataEmptyV2.index.v1compat()) } @Test fun testMin() { - testConversation("$ASSET_PATH/index-min-v1.json", TestDataMinV2.index.v1compat()) + testConversation("index-min-v1.json", TestDataMinV2.index.v1compat()) } @Test fun testMid() { - testConversation("$ASSET_PATH/index-mid-v1.json", TestDataMidV2.indexCompat) + testConversation("index-mid-v1.json", TestDataMidV2.indexCompat) } @Test fun testMax() { - testConversation("$ASSET_PATH/index-max-v1.json", TestDataMaxV2.indexCompat) + testConversation("index-max-v1.json", TestDataMaxV2.indexCompat) } private fun testConversation(file: String, expectedIndex: IndexV2) { diff --git a/libs/index/src/commonTest/kotlin/org/fdroid/index/v1/IndexV1Test.kt b/libs/index/src/commonTest/kotlin/org/fdroid/index/v1/IndexV1Test.kt index 49b393c4f..25e242d97 100644 --- a/libs/index/src/commonTest/kotlin/org/fdroid/index/v1/IndexV1Test.kt +++ b/libs/index/src/commonTest/kotlin/org/fdroid/index/v1/IndexV1Test.kt @@ -6,7 +6,6 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlinx.serialization.SerializationException -import org.fdroid.index.ASSET_PATH import org.fdroid.index.IndexParser.parseV1 import org.fdroid.test.TestDataEmptyV1 import org.fdroid.test.TestDataMaxV1 @@ -17,7 +16,7 @@ internal class IndexV1Test { @Test fun testIndexEmptyV1() { - val indexRes = Resource("$ASSET_PATH/index-empty-v1.json") + val indexRes = Resource("index-empty-v1.json") val indexStr = indexRes.readText() val index = parseV1(indexStr) assertEquals(TestDataEmptyV1.index, index) @@ -25,7 +24,7 @@ internal class IndexV1Test { @Test fun testIndexMinV1() { - val indexRes = Resource("$ASSET_PATH/index-min-v1.json") + val indexRes = Resource("index-min-v1.json") val indexStr = indexRes.readText() val index = parseV1(indexStr) assertEquals(TestDataMinV1.index, index) @@ -33,7 +32,7 @@ internal class IndexV1Test { @Test fun testIndexMidV1() { - val indexRes = Resource("$ASSET_PATH/index-mid-v1.json") + val indexRes = Resource("index-mid-v1.json") val indexStr = indexRes.readText() val index = parseV1(indexStr) assertEquals(TestDataMidV1.index, index) @@ -41,7 +40,7 @@ internal class IndexV1Test { @Test fun testIndexMaxV1() { - val indexRes = Resource("$ASSET_PATH/index-max-v1.json") + val indexRes = Resource("index-max-v1.json") val indexStr = indexRes.readText() val index = parseV1(indexStr) assertEquals(TestDataMaxV1.index, index) @@ -123,14 +122,14 @@ internal class IndexV1Test { @Test fun testGuardianProjectV1() { - val indexRes = Resource("$ASSET_PATH/guardianproject_index-v1.json") + val indexRes = Resource("guardianproject_index-v1.json") val indexStr = indexRes.readText() parseV1(indexStr) } @Test fun testLocalizedV1() { - val indexRes = Resource("$ASSET_PATH/localized.json") + val indexRes = Resource("localized.json") val indexStr = indexRes.readText() parseV1(indexStr) } diff --git a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/EntryTest.kt b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/EntryTest.kt index 67b6764e7..0a8719169 100644 --- a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/EntryTest.kt +++ b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/EntryTest.kt @@ -5,7 +5,6 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlinx.serialization.SerializationException -import org.fdroid.index.ASSET_PATH import org.fdroid.index.IndexParser import org.fdroid.test.TestDataEntry import org.junit.Test @@ -14,22 +13,22 @@ internal class EntryTest { @Test fun testEmpty() { - testEntryEquality("$ASSET_PATH/entry-empty-v2.json", TestDataEntry.empty) + testEntryEquality("entry-empty-v2.json", TestDataEntry.empty) } @Test fun testEmptyToMin() { - testEntryEquality("$ASSET_PATH/diff-empty-min/$DATA_FILE_NAME", TestDataEntry.emptyToMin) + testEntryEquality("diff-empty-min/$DATA_FILE_NAME", TestDataEntry.emptyToMin) } @Test fun testEmptyToMid() { - testEntryEquality("$ASSET_PATH/diff-empty-mid/$DATA_FILE_NAME", TestDataEntry.emptyToMid) + testEntryEquality("diff-empty-mid/$DATA_FILE_NAME", TestDataEntry.emptyToMid) } @Test fun testEmptyToMax() { - testEntryEquality("$ASSET_PATH/diff-empty-max/$DATA_FILE_NAME", TestDataEntry.emptyToMax) + testEntryEquality("diff-empty-max/$DATA_FILE_NAME", TestDataEntry.emptyToMax) } @Test diff --git a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2DiffStreamProcessorTest.kt b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2DiffStreamProcessorTest.kt index 344a55ad9..f13f50bff 100644 --- a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2DiffStreamProcessorTest.kt +++ b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2DiffStreamProcessorTest.kt @@ -10,8 +10,8 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject -import org.fdroid.index.ASSET_PATH import org.fdroid.index.IndexParser +import org.fdroid.test.TestUtils.ASSET_PATH import org.junit.Test internal class IndexV2DiffStreamProcessorTest { diff --git a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2Test.kt b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2Test.kt index 6227722cf..1b994386e 100644 --- a/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2Test.kt +++ b/libs/index/src/commonTest/kotlin/org/fdroid/index/v2/IndexV2Test.kt @@ -6,7 +6,6 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlinx.serialization.SerializationException -import org.fdroid.index.ASSET_PATH import org.fdroid.index.IndexParser.parseV2 import org.fdroid.test.TestDataEmptyV2 import org.fdroid.test.TestDataMaxV2 @@ -17,27 +16,27 @@ internal class IndexV2Test { @Test fun testEmpty() { - testIndexEquality("$ASSET_PATH/index-empty-v2.json", TestDataEmptyV2.index) + testIndexEquality("index-empty-v2.json", TestDataEmptyV2.index) } @Test fun testMin() { - testIndexEquality("$ASSET_PATH/index-min-v2.json", TestDataMinV2.index) + testIndexEquality("index-min-v2.json", TestDataMinV2.index) } @Test fun testMinReordered() { - testIndexEquality("$ASSET_PATH/index-min-reordered-v2.json", TestDataMinV2.index) + testIndexEquality("index-min-reordered-v2.json", TestDataMinV2.index) } @Test fun testMid() { - testIndexEquality("$ASSET_PATH/index-mid-v2.json", TestDataMidV2.index) + testIndexEquality("index-mid-v2.json", TestDataMidV2.index) } @Test fun testMax() { - testIndexEquality("$ASSET_PATH/index-max-v2.json", TestDataMaxV2.index) + testIndexEquality("index-max-v2.json", TestDataMaxV2.index) } @Test diff --git a/libs/sharedTest/build.gradle.kts b/libs/sharedTest/build.gradle.kts index c2535f684..4c79f64c7 100644 --- a/libs/sharedTest/build.gradle.kts +++ b/libs/sharedTest/build.gradle.kts @@ -1,28 +1,29 @@ plugins { - alias(libs.plugins.jetbrains.kotlin.android) - alias(libs.plugins.android.library) + alias(libs.plugins.jetbrains.kotlin.multiplatform) + alias(libs.plugins.android.multiplatform.library) alias(libs.plugins.ktfmt) } -// not really an Android library, but index is not publishing for JVM at the moment -android { - namespace = "org.fdroid.test" - compileSdk = libs.versions.compileSdk.get().toInt() - defaultConfig { minSdk = 21 } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +kotlin { + jvm() + android { + namespace = "org.fdroid.test" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = 21 + compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } + } + sourceSets { + commonMain { + resources.srcDir("src/commonMain/resources") + dependencies { + implementation(project(":libs:download")) + implementation(project(":libs:index")) + + implementation(libs.kotlin.test) + implementation(libs.kotlinx.serialization.json) + } + } } } -kotlin { compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } - -dependencies { - implementation(project(":libs:download")) - implementation(project(":libs:index")) - - implementation(libs.kotlin.test) - implementation(libs.kotlinx.serialization.json) -} - ktfmt { googleStyle() } diff --git a/libs/sharedTest/src/main/AndroidManifest.xml b/libs/sharedTest/src/commonMain/AndroidManifest.xml similarity index 100% rename from libs/sharedTest/src/main/AndroidManifest.xml rename to libs/sharedTest/src/commonMain/AndroidManifest.xml diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/DiffUtils.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/DiffUtils.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/DiffUtils.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/DiffUtils.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestAppUtils.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestAppUtils.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestAppUtils.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestAppUtils.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataEntry.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataEntry.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataEntry.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataEntry.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV1.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataV1.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV1.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataV1.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataV2.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestDataV2.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestDataV2.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestRepoUtils.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestRepoUtils.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestRepoUtils.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestRepoUtils.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestUtils.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestUtils.kt similarity index 88% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestUtils.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestUtils.kt index 7fae843de..f4dc131b3 100644 --- a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestUtils.kt +++ b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestUtils.kt @@ -1,5 +1,6 @@ package org.fdroid.test +import java.io.InputStream import kotlin.random.Random import org.fdroid.index.v2.IndexV2 import org.fdroid.index.v2.LocalizedFileListV2 @@ -8,6 +9,13 @@ import org.fdroid.index.v2.Screenshots object TestUtils { + const val ASSET_PATH = "../sharedTest/src/commonMain/resources" + + fun getRes(file: String): InputStream { + val classLoader = TestUtils.javaClass.classLoader ?: error("no classloader") + return classLoader.getResourceAsStream(file) ?: error("no resource $file") + } + fun String.decodeHex(): ByteArray { check(length % 2 == 0) { "Must have an even length" } return chunked(2).map { it.toInt(16).toByte() }.toByteArray() diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/TestVersionUtils.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestVersionUtils.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/TestVersionUtils.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/TestVersionUtils.kt diff --git a/libs/sharedTest/src/main/kotlin/org/fdroid/test/VerifierConstants.kt b/libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/VerifierConstants.kt similarity index 100% rename from libs/sharedTest/src/main/kotlin/org/fdroid/test/VerifierConstants.kt rename to libs/sharedTest/src/commonMain/kotlin/org/fdroid/test/VerifierConstants.kt diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/1337.json b/libs/sharedTest/src/commonMain/resources/diff-empty-max/1337.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-max/1337.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-max/1337.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/23.json b/libs/sharedTest/src/commonMain/resources/diff-empty-max/23.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-max/23.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-max/23.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/42.json b/libs/sharedTest/src/commonMain/resources/diff-empty-max/42.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-max/42.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-max/42.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/entry.jar b/libs/sharedTest/src/commonMain/resources/diff-empty-max/entry.jar similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-max/entry.jar rename to libs/sharedTest/src/commonMain/resources/diff-empty-max/entry.jar diff --git a/libs/sharedTest/src/main/assets/diff-empty-max/entry.json b/libs/sharedTest/src/commonMain/resources/diff-empty-max/entry.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-max/entry.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-max/entry.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-mid/23.json b/libs/sharedTest/src/commonMain/resources/diff-empty-mid/23.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-mid/23.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-mid/23.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-mid/42.json b/libs/sharedTest/src/commonMain/resources/diff-empty-mid/42.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-mid/42.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-mid/42.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-mid/entry.jar b/libs/sharedTest/src/commonMain/resources/diff-empty-mid/entry.jar similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-mid/entry.jar rename to libs/sharedTest/src/commonMain/resources/diff-empty-mid/entry.jar diff --git a/libs/sharedTest/src/main/assets/diff-empty-mid/entry.json b/libs/sharedTest/src/commonMain/resources/diff-empty-mid/entry.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-mid/entry.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-mid/entry.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-min/23.json b/libs/sharedTest/src/commonMain/resources/diff-empty-min/23.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-min/23.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-min/23.json diff --git a/libs/sharedTest/src/main/assets/diff-empty-min/entry.jar b/libs/sharedTest/src/commonMain/resources/diff-empty-min/entry.jar similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-min/entry.jar rename to libs/sharedTest/src/commonMain/resources/diff-empty-min/entry.jar diff --git a/libs/sharedTest/src/main/assets/diff-empty-min/entry.json b/libs/sharedTest/src/commonMain/resources/diff-empty-min/entry.json similarity index 100% rename from libs/sharedTest/src/main/assets/diff-empty-min/entry.json rename to libs/sharedTest/src/commonMain/resources/diff-empty-min/entry.json diff --git a/libs/sharedTest/src/main/assets/entry-empty-v2.json b/libs/sharedTest/src/commonMain/resources/entry-empty-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/entry-empty-v2.json rename to libs/sharedTest/src/commonMain/resources/entry-empty-v2.json diff --git a/libs/sharedTest/src/main/assets/guardianproject_entry.jar b/libs/sharedTest/src/commonMain/resources/guardianproject_entry.jar similarity index 100% rename from libs/sharedTest/src/main/assets/guardianproject_entry.jar rename to libs/sharedTest/src/commonMain/resources/guardianproject_entry.jar diff --git a/libs/sharedTest/src/main/assets/guardianproject_index-v1.json b/libs/sharedTest/src/commonMain/resources/guardianproject_index-v1.json similarity index 100% rename from libs/sharedTest/src/main/assets/guardianproject_index-v1.json rename to libs/sharedTest/src/commonMain/resources/guardianproject_index-v1.json diff --git a/libs/sharedTest/src/main/assets/index-empty-v1.json b/libs/sharedTest/src/commonMain/resources/index-empty-v1.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-empty-v1.json rename to libs/sharedTest/src/commonMain/resources/index-empty-v1.json diff --git a/libs/sharedTest/src/main/assets/index-empty-v2.json b/libs/sharedTest/src/commonMain/resources/index-empty-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-empty-v2.json rename to libs/sharedTest/src/commonMain/resources/index-empty-v2.json diff --git a/libs/sharedTest/src/main/assets/index-max-v1.json b/libs/sharedTest/src/commonMain/resources/index-max-v1.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-max-v1.json rename to libs/sharedTest/src/commonMain/resources/index-max-v1.json diff --git a/libs/sharedTest/src/main/assets/index-max-v2.json b/libs/sharedTest/src/commonMain/resources/index-max-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-max-v2.json rename to libs/sharedTest/src/commonMain/resources/index-max-v2.json diff --git a/libs/sharedTest/src/main/assets/index-mid-v1.json b/libs/sharedTest/src/commonMain/resources/index-mid-v1.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-mid-v1.json rename to libs/sharedTest/src/commonMain/resources/index-mid-v1.json diff --git a/libs/sharedTest/src/main/assets/index-mid-v2.json b/libs/sharedTest/src/commonMain/resources/index-mid-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-mid-v2.json rename to libs/sharedTest/src/commonMain/resources/index-mid-v2.json diff --git a/libs/sharedTest/src/main/assets/index-min-reordered-v2.json b/libs/sharedTest/src/commonMain/resources/index-min-reordered-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-min-reordered-v2.json rename to libs/sharedTest/src/commonMain/resources/index-min-reordered-v2.json diff --git a/libs/sharedTest/src/main/assets/index-min-v1.json b/libs/sharedTest/src/commonMain/resources/index-min-v1.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-min-v1.json rename to libs/sharedTest/src/commonMain/resources/index-min-v1.json diff --git a/libs/sharedTest/src/main/assets/index-min-v2.json b/libs/sharedTest/src/commonMain/resources/index-min-v2.json similarity index 100% rename from libs/sharedTest/src/main/assets/index-min-v2.json rename to libs/sharedTest/src/commonMain/resources/index-min-v2.json diff --git a/libs/sharedTest/src/main/assets/localized.json b/libs/sharedTest/src/commonMain/resources/localized.json similarity index 100% rename from libs/sharedTest/src/main/assets/localized.json rename to libs/sharedTest/src/commonMain/resources/localized.json diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_corrupt_app_package_name_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_corrupt_app_package_name_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_corrupt_app_package_name_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_corrupt_app_package_name_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_corrupt_package_name_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_corrupt_package_name_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_corrupt_package_name_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_corrupt_package_name_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_no-.RSA_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-.RSA_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_no-.RSA_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-.RSA_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_no-.SF_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-.SF_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_no-.SF_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-.SF_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_no-MANIFEST.MF_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-MANIFEST.MF_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_no-MANIFEST.MF_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-MANIFEST.MF_index-v1.jar diff --git a/libs/sharedTest/src/main/assets/testy.at.or.at_no-signature_index-v1.jar b/libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-signature_index-v1.jar similarity index 100% rename from libs/sharedTest/src/main/assets/testy.at.or.at_no-signature_index-v1.jar rename to libs/sharedTest/src/commonMain/resources/testy.at.or.at_no-signature_index-v1.jar